feat: start fresh research chats while preserving deck context

This commit is contained in:
2026-09-24 18:03:29 +09:00
parent b2db67ea1c
commit 9c3917541b
8 changed files with 246 additions and 8 deletions
+63 -7
View File
@@ -7,6 +7,7 @@ import { isResearchActive, type ResearchRun } from './model'
import {
cancelResearch,
loadResearchStatus,
resetResearch,
startResearch,
} from './server-functions'
import './research-panel.css'
@@ -34,6 +35,7 @@ export function ResearchPanel({
const fetchStatus = useServerFn(loadResearchStatus)
const start = useServerFn(startResearch)
const cancel = useServerFn(cancelResearch)
const reset = useServerFn(resetResearch)
const client = useQueryClient()
const status = useQuery({
queryKey: ['research-status'],
@@ -44,6 +46,8 @@ export function ResearchPanel({
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string>()
const [reconnecting, setReconnecting] = useState(false)
const [draftResetVersion, setDraftResetVersion] = useState(0)
const previousRunId = useRef<string | null>(null)
const attemptedVersion = useRef<string | null>(null)
const pendingTurn = useRef<{
runId?: string
@@ -61,6 +65,15 @@ export function ResearchPanel({
)
const request = useRef<{ signature: string; id: string } | null>(null)
useEffect(() => {
if (!run && previousRunId.current) {
setDraftResetVersion((value) => value + 1)
request.current = null
pendingTurn.current = null
}
previousRunId.current = run?.id ?? null
}, [run])
useEffect(() => {
const events = new EventSource('/api/research/events')
events.onopen = () => setReconnecting(false)
@@ -128,9 +141,7 @@ export function ResearchPanel({
client.setQueryData<{ configured: boolean; run: ResearchRun | null }>(
['research-status'],
(existing) =>
existing !== beforeStart && existing?.run?.id === next.id
? existing
: { configured: true, run: next },
existing !== beforeStart ? existing : { configured: true, run: next },
)
return true
} catch {
@@ -146,14 +157,19 @@ export function ResearchPanel({
async function stop() {
if (!run) return
const beforeCancel = client.getQueryData(['research-status'])
setBusy(true)
setError(undefined)
try {
const next = await cancel({ data: { id: run.id } })
client.setQueryData(['research-status'], {
configured: status.data?.configured ?? true,
run: next,
})
client.setQueryData(['research-status'], (existing: unknown) =>
existing === beforeCancel
? {
configured: status.data?.configured ?? true,
run: next,
}
: existing,
)
} catch {
setError('調査を中止できませんでした。もう一度お試しください。')
} finally {
@@ -161,6 +177,28 @@ export function ResearchPanel({
}
}
async function newChat() {
if (!run || isResearchActive(run)) return
const beforeReset = client.getQueryData(['research-status'])
setBusy(true)
setError(undefined)
try {
const next = await reset({ data: { id: run.id } })
client.setQueryData(['research-status'], (existing: unknown) =>
existing === beforeReset ? next : existing,
)
request.current = null
pendingTurn.current = null
setDraftResetVersion((value) => value + 1)
} catch {
setError(
'新しいチャットを開始できませんでした。会話と入力内容は残っています。もう一度お試しください。',
)
} finally {
setBusy(false)
}
}
async function openDeck(deck: Deck) {
setBusy(true)
setError(undefined)
@@ -179,6 +217,18 @@ export function ResearchPanel({
<aside className="research-panel" aria-label="調査チャット">
<header className="research-heading">
<h2>調査チャット</h2>
<button
type="button"
disabled={busy || !run || isResearchActive(run)}
title={
isResearchActive(run)
? '調査を中止してから新しいチャットを開始できます'
: undefined
}
onClick={() => void newChat()}
>
新しいチャット
</button>
<p>会話に合わせて右のデッキが変わります。</p>
</header>
{reconnecting && (
@@ -214,6 +264,7 @@ export function ResearchPanel({
/>
)}
<ResearchComposer
resetVersion={draftResetVersion}
connections={connections}
contextTitle={contextDeck.title}
disabled={
@@ -272,11 +323,13 @@ function ResearchConversation({
}
function ResearchComposer({
resetVersion,
connections,
contextTitle,
disabled,
onSend,
}: {
resetVersion: number
connections: Connection[]
contextTitle: string
disabled: boolean
@@ -286,6 +339,9 @@ function ResearchComposer({
(connection) => connection.status === 'connected',
)
const [topic, setTopic] = useState('')
useEffect(() => {
if (resetVersion > 0) setTopic('')
}, [resetVersion])
const [selected, setSelected] = useState<string[] | null>(null)
const selectedIds =
selected ?? available.slice(0, 6).map((connection) => connection.id)