ci/woodpecker/push/publish Pipeline failed
Co-authored-by: Jason Woltje <[email protected]>
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
import type { ReactElement } from 'react';
|
|
import { CommandsPanel } from '@/spa/chat/commands-panel';
|
|
import { Composer } from '@/spa/chat/composer';
|
|
import { MessageTranscript } from '@/spa/chat/message-transcript';
|
|
import { asFiniteNumberOrNull, asString } from '@/spa/chat/runtime-guards';
|
|
import { SessionPanel } from '@/spa/chat/session-panel';
|
|
import { ToolCallList } from '@/spa/chat/tool-call-list';
|
|
import { useChatConnection } from '@/spa/chat/use-chat-connection';
|
|
import { useHarnessSelection } from '@/spa/chat/use-harness-selection';
|
|
|
|
/** Renders a real value normally, but an honest "unavailable" label instead
|
|
* of a fabricated `0` for a missing/malformed count — a real `0 tokens` and
|
|
* an unknown token count must never look the same. */
|
|
function formatTokens(value: unknown): string {
|
|
const tokens = asFiniteNumberOrNull(value);
|
|
return tokens === null ? 'tokens unavailable' : `${tokens} tokens`;
|
|
}
|
|
|
|
/** Same honesty guarantee as `formatTokens`, for cost. */
|
|
function formatCost(value: unknown): string {
|
|
const cost = asFiniteNumberOrNull(value);
|
|
return cost === null ? 'cost unavailable' : `$${cost.toFixed(4)}`;
|
|
}
|
|
|
|
export function ChatPage(): ReactElement {
|
|
const { state, actions } = useChatConnection();
|
|
const harness = useHarnessSelection();
|
|
const hasConversation = state.conversationId !== null;
|
|
|
|
return (
|
|
<div className="flex h-[calc(100vh-3.5rem)] min-h-0 flex-col overflow-hidden md:h-screen">
|
|
<header className="border-b px-4 py-3">
|
|
<h1 className="text-lg font-semibold">Chat</h1>
|
|
</header>
|
|
|
|
{state.systemReload ? (
|
|
<div role="status" className="border-b px-4 py-2 text-sm">
|
|
{asString(state.systemReload.message)}
|
|
</div>
|
|
) : null}
|
|
|
|
{state.error ? (
|
|
<div role="alert" className="border-b px-4 py-2 text-sm">
|
|
{asString(state.error)}
|
|
</div>
|
|
) : null}
|
|
|
|
{state.ack ? (
|
|
<div role="status" className="border-b px-4 py-1 text-xs opacity-70">
|
|
Message accepted · conversation {asString(state.ack.conversationId, 'unknown')} · id{' '}
|
|
{asString(state.ack.messageId, 'unknown')}
|
|
</div>
|
|
) : null}
|
|
|
|
<SessionPanel sessionInfo={state.sessionInfo} onSetThinking={actions.setThinking} />
|
|
|
|
<MessageTranscript messages={state.messages} streaming={state.streaming} text={state.text} />
|
|
|
|
{state.thinking ? (
|
|
<section aria-label="Thinking" className="px-4 pb-2 text-xs italic opacity-80">
|
|
{state.thinking}
|
|
</section>
|
|
) : null}
|
|
|
|
<ToolCallList tools={state.tools} />
|
|
|
|
{state.usage ? (
|
|
<div aria-label="Usage" className="px-4 pb-2 text-xs opacity-80">
|
|
{formatTokens(state.usage.tokens?.total)} · {formatCost(state.usage.cost)} ·{' '}
|
|
{asString(state.usage.provider, 'unknown')}/{asString(state.usage.modelId, 'unknown')}
|
|
</div>
|
|
) : null}
|
|
|
|
<CommandsPanel
|
|
manifest={state.manifest}
|
|
results={state.commandResults}
|
|
approval={state.approval}
|
|
pendingApproval={state.pendingApproval}
|
|
hasConversation={hasConversation}
|
|
onExecute={actions.executeCommand}
|
|
onApprove={actions.approveCommand}
|
|
onRunApproved={actions.runApprovedCommand}
|
|
/>
|
|
|
|
<Composer
|
|
onSend={actions.sendMessage}
|
|
onStop={actions.abort}
|
|
streaming={state.streaming}
|
|
sending={state.sending}
|
|
hasConversation={hasConversation}
|
|
harness={harness}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|