system prompt
This commit is contained in:
@@ -63,6 +63,10 @@ export const useClaude = (initialSessionId?: string, initialModel?: string | nul
|
|||||||
if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`);
|
if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'system:prompt':
|
||||||
|
setMessages((prev) => [...prev, { role: 'system', text: msg.text }]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'assistant:partial':
|
case 'assistant:partial':
|
||||||
streamingRef.current += msg.text;
|
streamingRef.current += msg.text;
|
||||||
flushStreaming();
|
flushStreaming();
|
||||||
|
|||||||
@@ -5,6 +5,26 @@ import { useVisibleOpenCodeModels } from '@/state/useModels';
|
|||||||
import { useChatSessions } from '@/state/useChatSessions';
|
import { useChatSessions } from '@/state/useChatSessions';
|
||||||
import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat';
|
import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat';
|
||||||
|
|
||||||
|
const SYSTEM_RE = /^<system>([\s\S]*?)<\/system>\s*/;
|
||||||
|
|
||||||
|
const splitSystemBlocks = (messages: ChatMessage[]): ChatMessage[] => {
|
||||||
|
const result: ChatMessage[] = [];
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.role !== 'user') {
|
||||||
|
result.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const match = msg.text.match(SYSTEM_RE);
|
||||||
|
if (!match) {
|
||||||
|
result.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push({ role: 'user', text: msg.text.slice(match[0]!.length), images: msg.images });
|
||||||
|
result.push({ role: 'system', text: match[1]!.trim() });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
type UseOpenCodeOptions = {
|
type UseOpenCodeOptions = {
|
||||||
replaceUrl?: boolean;
|
replaceUrl?: boolean;
|
||||||
taskInfo?: TaskInfo;
|
taskInfo?: TaskInfo;
|
||||||
@@ -68,6 +88,10 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
|
|||||||
if (replaceUrl) window.history.replaceState(null, '', `/chat/opencode/${msg.sessionId}`);
|
if (replaceUrl) window.history.replaceState(null, '', `/chat/opencode/${msg.sessionId}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'system:prompt':
|
||||||
|
setMessages((prev) => [...prev, { role: 'system', text: msg.text }]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'assistant:partial':
|
case 'assistant:partial':
|
||||||
streamingRef.current += msg.text;
|
streamingRef.current += msg.text;
|
||||||
flushStreaming();
|
flushStreaming();
|
||||||
@@ -146,7 +170,7 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
|
|||||||
if (!initialSessionId) return;
|
if (!initialSessionId) return;
|
||||||
getMessages('opencode', initialSessionId)
|
getMessages('opencode', initialSessionId)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (Array.isArray(data) && data.length > 0) setMessages(data);
|
if (Array.isArray(data) && data.length > 0) setMessages(splitSystemBlocks(data));
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [initialSessionId]);
|
}, [initialSessionId]);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type ClientMessage =
|
|||||||
// Server → Client
|
// Server → Client
|
||||||
export type ServerMessage =
|
export type ServerMessage =
|
||||||
| { type: 'session:init'; sessionId: string; model: string }
|
| { type: 'session:init'; sessionId: string; model: string }
|
||||||
|
| { type: 'system:prompt'; text: string }
|
||||||
| { type: 'assistant:text'; text: string }
|
| { type: 'assistant:text'; text: string }
|
||||||
| { type: 'assistant:partial'; text: string }
|
| { type: 'assistant:partial'; text: string }
|
||||||
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }
|
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ async function handleChat({
|
|||||||
const workingDir = state.cwd ?? homeDir;
|
const workingDir = state.cwd ?? homeDir;
|
||||||
|
|
||||||
const skillsAppend = await buildSkillsPrompt(ws.data.email);
|
const skillsAppend = await buildSkillsPrompt(ws.data.email);
|
||||||
|
if (skillsAppend) send(ws, { type: 'system:prompt', text: skillsAppend });
|
||||||
|
|
||||||
// Build prompt: use AsyncIterable<SDKUserMessage> with image content blocks when images are present
|
// Build prompt: use AsyncIterable<SDKUserMessage> with image content blocks when images are present
|
||||||
let promptInput: string | AsyncIterable<SDKUserMessage> = prompt;
|
let promptInput: string | AsyncIterable<SDKUserMessage> = prompt;
|
||||||
|
|||||||
@@ -401,7 +401,8 @@ async function handleChat({ ws, prompt, sessionId, model, attachmentIds, images,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const skillsAppend = await buildSkillsPrompt(ws.data.email);
|
const skillsAppend = await buildSkillsPrompt(ws.data.email);
|
||||||
const fullPrompt = skillsAppend ? `${skillsAppend}\n\n${prompt}` : prompt;
|
if (skillsAppend) send(ws, { type: 'system:prompt', text: skillsAppend });
|
||||||
|
const fullPrompt = skillsAppend ? `<system>${skillsAppend}</system>\n\n${prompt}` : prompt;
|
||||||
|
|
||||||
const parts: Record<string, unknown>[] = [];
|
const parts: Record<string, unknown>[] = [];
|
||||||
if (images?.length) {
|
if (images?.length) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ type MessageBubbleProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
|
export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
|
||||||
const text = formatText(message.text);
|
|
||||||
switch (message.role) {
|
switch (message.role) {
|
||||||
case 'user':
|
case 'user':
|
||||||
return (
|
return (
|
||||||
@@ -20,18 +19,32 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
|
|||||||
{message.images?.map((img, i) => (
|
{message.images?.map((img, i) => (
|
||||||
<img key={i} src={img.dataUrl} alt={img.filename} className="max-w-full max-h-64 rounded-lg mb-2" />
|
<img key={i} src={img.dataUrl} alt={img.filename} className="max-w-full max-h-64 rounded-lg mb-2" />
|
||||||
))}
|
))}
|
||||||
<div className="whitespace-pre-wrap">{text}</div>
|
<div className="whitespace-pre-wrap">{message.text}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'system':
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<details className="max-w-[85%] rounded-2xl bg-duck-dark/5 border border-duck-dark/10 px-4 py-2 text-sm">
|
||||||
|
<summary className="text-xs text-duck-dark/40 hover:text-duck-dark/60 cursor-pointer select-none">
|
||||||
|
System prompt
|
||||||
|
</summary>
|
||||||
|
<div className="mt-1.5 text-xs text-duck-dark/40 whitespace-pre-wrap border-l-2 border-duck-dark/10 pl-2 max-h-48 overflow-y-auto">
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
case 'assistant':
|
case 'assistant':
|
||||||
if (!text) return null;
|
if (!message.text) return null;
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="max-w-[85%] rounded-2xl rounded-tl-sm bg-white/80 border border-duck-dark/10 px-4 py-2.5 text-sm text-duck-dark prose prose-sm max-w-none prose-pre:bg-gray-900 prose-pre:text-green-400 prose-code:text-duck-teal prose-code:before:content-none prose-code:after:content-none">
|
<div className="max-w-[85%] rounded-2xl rounded-tl-sm bg-white/80 border border-duck-dark/10 px-4 py-2.5 text-sm text-duck-dark prose prose-sm max-w-none prose-pre:bg-gray-900 prose-pre:text-green-400 prose-code:text-duck-teal prose-code:before:content-none prose-code:after:content-none">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
||||||
{text}
|
{message.text}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,23 +71,13 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="max-w-[80%] rounded-2xl bg-red-50 border border-red-200 px-4 py-2.5 text-sm text-red-700">
|
<div className="max-w-[80%] rounded-2xl bg-red-50 border border-red-200 px-4 py-2.5 text-sm text-red-700">
|
||||||
{text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatText(value: unknown): string {
|
|
||||||
if (typeof value === 'string') return value;
|
|
||||||
if (value == null) return '';
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamingBubbleProps = {
|
type StreamingBubbleProps = {
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type SessionEntry = {
|
|||||||
export type ChatMessage =
|
export type ChatMessage =
|
||||||
| { role: 'user'; text: string; images?: { filename: string; dataUrl: string }[] }
|
| { role: 'user'; text: string; images?: { filename: string; dataUrl: string }[] }
|
||||||
| { role: 'assistant'; text: string }
|
| { role: 'assistant'; text: string }
|
||||||
|
| { role: 'system'; text: string }
|
||||||
| {
|
| {
|
||||||
role: 'tool';
|
role: 'tool';
|
||||||
toolName: string;
|
toolName: string;
|
||||||
@@ -29,6 +30,7 @@ export type TaskInfo = {
|
|||||||
|
|
||||||
export type ServerMessage =
|
export type ServerMessage =
|
||||||
| { type: 'session:init'; sessionId: string; model: string }
|
| { type: 'session:init'; sessionId: string; model: string }
|
||||||
|
| { type: 'system:prompt'; text: string }
|
||||||
| { type: 'assistant:text'; text: string }
|
| { type: 'assistant:text'; text: string }
|
||||||
| { type: 'assistant:partial'; text: string }
|
| { type: 'assistant:partial'; text: string }
|
||||||
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }
|
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }
|
||||||
|
|||||||
Reference in New Issue
Block a user