2026-05-06 03:25:28 +02:00
|
|
|
import React from 'react'
|
|
|
|
|
import { markdownToHTML } from './markdown'
|
|
|
|
|
import { enrichOllamaErrorText, splitThinkBlocks } from './chatText'
|
2026-05-06 04:40:01 +02:00
|
|
|
import desktopApi from './desktop/desktopApi'
|
2026-05-06 03:25:28 +02:00
|
|
|
|
|
|
|
|
export default function AssistantMessageContent({ content, streamOutput, sources }) {
|
|
|
|
|
const displayContent = enrichOllamaErrorText(content || '')
|
|
|
|
|
const { think, answer } = splitThinkBlocks(displayContent)
|
|
|
|
|
const [open, setOpen] = React.useState(false)
|
|
|
|
|
const showThinkButton = !!think
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="assistant-message">
|
|
|
|
|
{showThinkButton && (
|
|
|
|
|
<div className="assistant-thoughts">
|
|
|
|
|
<button
|
|
|
|
|
className="think-toggle"
|
|
|
|
|
onClick={() => setOpen(o => !o)}
|
|
|
|
|
aria-expanded={open ? 'true' : 'false'}
|
|
|
|
|
aria-controls="think-content"
|
|
|
|
|
>
|
|
|
|
|
<span className="think-toggle-icon" aria-hidden="true">
|
|
|
|
|
{open ? '▾' : '▸'}
|
|
|
|
|
</span>
|
|
|
|
|
Thoughts
|
|
|
|
|
</button>
|
|
|
|
|
{open && (
|
|
|
|
|
<div
|
|
|
|
|
id="think-content"
|
|
|
|
|
className="think-content"
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: markdownToHTML(think) }}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div
|
|
|
|
|
className="msg-content"
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: markdownToHTML(answer || displayContent || '') }}
|
|
|
|
|
/>
|
|
|
|
|
{Array.isArray(sources) && sources.length > 0 && (
|
|
|
|
|
<div className="msg-sources chips">
|
|
|
|
|
{sources.map((u, i) => {
|
|
|
|
|
let label = u
|
|
|
|
|
let isFile = false
|
|
|
|
|
try {
|
|
|
|
|
const parsed = new URL(u)
|
|
|
|
|
if (parsed.protocol === 'file:') {
|
|
|
|
|
isFile = true
|
|
|
|
|
const parts = parsed.pathname.split('/').filter(Boolean)
|
|
|
|
|
label = decodeURIComponent(parts[parts.length - 1] || u)
|
|
|
|
|
} else {
|
|
|
|
|
const host = parsed.hostname || u
|
|
|
|
|
label = host.replace(/^www\./i, '')
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
return (
|
|
|
|
|
<a
|
|
|
|
|
key={u + i}
|
|
|
|
|
className="chip"
|
|
|
|
|
href={u}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
title={u}
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
if (!isFile) return
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
try {
|
|
|
|
|
const parsed = new URL(u)
|
2026-05-06 04:39:39 +02:00
|
|
|
desktopApi?.openPath?.(decodeURIComponent(parsed.pathname))
|
2026-05-06 03:25:28 +02:00
|
|
|
} catch {}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</a>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|