feat(gui): add ask-user question composer
This commit is contained in:
@@ -62,6 +62,8 @@ export function apply(ctx: Context): void {
|
||||
const layout = need<LayoutService>(ctx, 'layout')
|
||||
const i18n = need<I18nService>(ctx, 'i18n')
|
||||
const slots = need<SlotsService>(ctx, 'slots')
|
||||
slots.define('conversation.composer', { kind: 'keyed', scope: 'session' })
|
||||
const composerSlots = scopedSlots(slots.core, 'conversation.composer')
|
||||
|
||||
const conversation = new ConversationService(ctx)
|
||||
const toolviews = new ToolViewRegistry()
|
||||
@@ -153,6 +155,7 @@ export function apply(ctx: Context): void {
|
||||
}
|
||||
return createElement(Fragment, null, ...children)
|
||||
},
|
||||
slots: composerSlots,
|
||||
}
|
||||
return injected
|
||||
}
|
||||
|
||||
@@ -270,7 +270,9 @@ export function createChatView(deps: ChatViewDeps): FC<ConvViewProps> {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{pending.map((item) => <PendingCard key={item.rpcId} item={item} />)}
|
||||
{pending.map((item) => item.kind === 'approval'
|
||||
? <PendingCard key={item.rpcId} item={item} />
|
||||
: null)}
|
||||
</div>
|
||||
</div>
|
||||
{!atBottom && (
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
// PendingCard: approval/question placeholder card (visible, not answerable —
|
||||
// the composer-takeover approval panel is a P-II item; wire pending semantics
|
||||
// already exist so the flow must show them).
|
||||
// PendingCard: approval placeholder card. Questions take over the composer.
|
||||
|
||||
import { memo } from 'react'
|
||||
import type { PendingInteraction } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { JsonBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import css from './PendingCard.module.css'
|
||||
|
||||
export interface PendingCardProps {
|
||||
item: PendingInteraction
|
||||
item: Extract<PendingInteraction, { kind: 'approval' }>
|
||||
}
|
||||
|
||||
export const PendingCard = memo(function PendingCard({ item }: PendingCardProps) {
|
||||
return (
|
||||
<div className={css.card}>
|
||||
{item.kind === 'approval' ? (
|
||||
<>
|
||||
<div className={css.title}>等待审批:<span className={css.mono}>{item.toolName}</span></div>
|
||||
{item.reason !== undefined && <div className={css.reason}>{item.reason}</div>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={css.title}>等待回答({item.questions.length} 题)</div>
|
||||
<JsonBlock label="问题内容" payload={item.questions} />
|
||||
</>
|
||||
)}
|
||||
<div className={css.title}>等待审批:<span className={css.mono}>{item.toolName}</span></div>
|
||||
{item.reason !== undefined && <div className={css.reason}>{item.reason}</div>}
|
||||
<div className={css.hint}>请在原客户端处理(web 端作答后续里程碑提供)</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* standard share & own injected share.
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import type { SessionId, SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { PendingInteraction, SessionId, SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ScopedSlots } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SnapshotSelectorHook, UseSession } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ConvOwnerProps, DetailsOwnerProps, EmptyOwnerProps } from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type { SelectionTarget, ViewEntry, ViewId } from './views.ts'
|
||||
@@ -38,6 +39,13 @@ export interface ConversationInjected {
|
||||
}
|
||||
/** Renders the active view's body (the owner closes over ConvViewProps assembly). */
|
||||
renderView: (entry: ViewEntry) => ReactNode
|
||||
/** Feature-owned composer replacements, dispatched by pending interaction kind. */
|
||||
slots: ScopedSlots<'conversation.composer'>
|
||||
}
|
||||
|
||||
/** Question-composer owner share supplied by ConversationRoot. */
|
||||
export interface QuestionComposerOwnerProps {
|
||||
interaction: Extract<PendingInteraction, { kind: 'question' }>
|
||||
}
|
||||
|
||||
/** Full conversation-slot component props: owner share & standard share & injected share. */
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
import type { ConversationService } from './service.ts'
|
||||
import type { ToolViewRegistry } from './toolviews/registry.ts'
|
||||
import type { QuestionComposerOwnerProps } from './contract/slots.ts'
|
||||
|
||||
export { apply, inject } from './apply.ts'
|
||||
export { ConversationService } from './service.ts'
|
||||
@@ -22,7 +23,7 @@ export type {
|
||||
} from './contract/toolview.ts'
|
||||
export type {
|
||||
ConversationInjected, ConversationSlotProps, DetailsInjected, DetailsSlotProps,
|
||||
EmptyStateInjected, EmptyStateSlotProps,
|
||||
EmptyStateInjected, EmptyStateSlotProps, QuestionComposerOwnerProps,
|
||||
} from './contract/slots.ts'
|
||||
|
||||
export { ConversationRoot } from './skeleton/ConversationRoot.tsx'
|
||||
@@ -40,3 +41,13 @@ declare module 'cordis' {
|
||||
toolviews: ToolViewRegistry
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface SlotMap {
|
||||
'conversation.composer': {
|
||||
kind: 'keyed'
|
||||
scope: 'session'
|
||||
owner: QuestionComposerOwnerProps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import type { PendingInteraction } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationSlotProps } from '../contract/slots.ts'
|
||||
import { InputBar } from './InputBar.tsx'
|
||||
import type { InputBarError } from './InputBar.tsx'
|
||||
@@ -20,7 +21,7 @@ import css from './ConversationRoot.module.css'
|
||||
export type ConversationRootProps = ConversationSlotProps
|
||||
|
||||
export function ConversationRoot({
|
||||
sessionId, useSession, useAncestry, views, useActiveView, composer, actions, renderView,
|
||||
sessionId, useSession, useAncestry, views, useActiveView, composer, actions, renderView, slots,
|
||||
}: ConversationRootProps) {
|
||||
useSyncExternalStore(views.subscribe, views.version)
|
||||
const list = views.list()
|
||||
@@ -33,6 +34,9 @@ export function ConversationRoot({
|
||||
const removed = useSession(s => (s as { removed: boolean }).removed)
|
||||
const promptError = useSession(s => (s as { promptError: { op: 'send' | 'stop'; error: { message: string; code: string } } | null }).promptError)
|
||||
const turns = useSession(s => countTurns(s as { nodes: readonly { kind: string }[] }))
|
||||
const question = useSession(s => (
|
||||
s as { pending: readonly PendingInteraction[] }
|
||||
).pending.find(item => item.kind === 'question'))
|
||||
|
||||
const error: InputBarError | null = promptError === null
|
||||
? null
|
||||
@@ -87,20 +91,37 @@ export function ConversationRoot({
|
||||
{active !== undefined && renderView(active)}
|
||||
</div>
|
||||
|
||||
<InputBar
|
||||
draft={draft}
|
||||
running={running}
|
||||
disabled={removed}
|
||||
error={error}
|
||||
variant="composer"
|
||||
onDraftChange={composer.setDraft}
|
||||
onSend={composer.send}
|
||||
onStop={composer.stop}
|
||||
/>
|
||||
{question?.kind === 'question'
|
||||
? slots.renderSlot('conversation.composer', { interaction: question }, {
|
||||
entryKey: 'question',
|
||||
fallback: <ComposerInput {...{ draft, running, removed, error, composer }} />,
|
||||
})
|
||||
: <ComposerInput {...{ draft, running, removed, error, composer }} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ComposerInput({ draft, running, removed, error, composer }: {
|
||||
draft: string
|
||||
running: boolean
|
||||
removed: boolean
|
||||
error: InputBarError | null
|
||||
composer: ConversationSlotProps['composer']
|
||||
}) {
|
||||
return (
|
||||
<InputBar
|
||||
draft={draft}
|
||||
running={running}
|
||||
disabled={removed}
|
||||
error={error}
|
||||
variant="composer"
|
||||
onDraftChange={composer.setDraft}
|
||||
onSend={composer.send}
|
||||
onStop={composer.stop}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Turn count = user message nodes in the window (display meta; exact host count deferred). */
|
||||
function countTurns(s: { nodes: readonly { kind: string }[] }): number {
|
||||
let n = 0
|
||||
|
||||
Reference in New Issue
Block a user