feat: refine mobile master agent sync and chat rendering

This commit is contained in:
kris
2026-04-18 04:51:50 +08:00
parent e0c0ea1814
commit 449f84fcbc
61 changed files with 7051 additions and 1075 deletions

View File

@@ -25,6 +25,7 @@ import type {
ThreadConversationExecutionConflict,
ThreadConversationExecutionConflictAction,
} from "@/lib/thread-execution-conflict";
import { parseChatMarkdown, type ChatMarkdownBlock } from "@/lib/chat-markdown";
import {
describeThreadConversationExecutionConflict,
labelForProjectConflictAllowPolicy,
@@ -907,13 +908,101 @@ export function ChatBubble({ message }: { message: Message }) {
{tag ? (
<div className="mb-2 text-[11px] font-semibold opacity-80">{tag}</div>
) : null}
{message.body}
<ChatBubbleMarkdown body={message.body} mine={mine} green={green} />
</div>
</div>
</div>
);
}
function ChatBubbleMarkdown({
body,
mine,
green,
}: {
body: string;
mine: boolean;
green: boolean;
}) {
const blocks = parseChatMarkdown(body);
return (
<div className="space-y-2 break-words">
{blocks.map((block, index) => (
<ChatMarkdownBlockView key={`${block.kind}-${index}`} block={block} mine={mine} green={green} />
))}
</div>
);
}
function ChatMarkdownBlockView({
block,
mine,
green,
}: {
block: ChatMarkdownBlock;
mine: boolean;
green: boolean;
}) {
const mutedClass = mine ? "text-white/82" : green ? "text-[#4E7A60]" : "text-[#57606A]";
const markerClass = mine ? "text-white/72" : green ? "text-[#44A064]" : "text-[#8C8C8C]";
switch (block.kind) {
case "heading":
return (
<div
className={clsx(
"font-semibold leading-6",
block.level === 1 ? "text-[16px]" : block.level === 2 ? "text-[15px]" : "text-[14px]",
)}
>
{block.text}
</div>
);
case "label":
return (
<div className="rounded-2xl bg-black/[0.035] px-3 py-2">
<div className={clsx("text-[12px] font-semibold", markerClass)}>{block.label}</div>
<div className="mt-1 whitespace-pre-wrap text-[14px] leading-6">{block.text}</div>
</div>
);
case "bullet":
return (
<div className="flex gap-2 leading-6">
<span className={markerClass}></span>
<span className="min-w-0 flex-1">{block.text}</span>
</div>
);
case "ordered":
return (
<div className="flex gap-2 leading-6">
<span className={clsx("tabular-nums", markerClass)}>{block.order}</span>
<span className="min-w-0 flex-1">{block.text}</span>
</div>
);
case "quote":
return (
<div className={clsx("border-l-2 pl-3 text-[14px] leading-6", mine ? "border-white/50" : "border-[#D8DEE4]", mutedClass)}>
{block.text}
</div>
);
case "code":
return (
<pre
className={clsx(
"overflow-x-auto rounded-2xl px-3 py-2 text-[12px] leading-5",
mine ? "bg-white/16 text-white" : "bg-[#F2F3F5] text-[#24292F]",
)}
>
<code>{block.text}</code>
</pre>
);
case "paragraph":
default:
return <div className="whitespace-pre-wrap leading-6">{block.text}</div>;
}
}
export function ProjectHeaderActions({ projectId }: { projectId: string }) {
return (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">

View File

@@ -12,6 +12,7 @@ import type {
UserMasterPrompt,
} from "@/lib/boss-data";
import type { MasterAgentChatPageAnchors } from "@/lib/master-agent-chat-menu";
import { getMasterAgentModelOptions } from "@/lib/master-agent-model-options";
import { formatTimestampLabel } from "@/lib/boss-projections";
type MemoryDraft = {
@@ -191,6 +192,7 @@ export function MasterAgentPromptMemoryClient({
});
const allMemories = useMemo(() => [...projectMemories, ...globalMemories], [projectMemories, globalMemories]);
const modelOptions = useMemo(() => getMasterAgentModelOptions(modelOverride), [modelOverride]);
const promptPreview = useMemo(() => {
const sections = [
globalPrompt.trim() ? `【管理员全局主提示词】\n${globalPrompt.trim()}` : null,
@@ -431,9 +433,11 @@ export function MasterAgentPromptMemoryClient({
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
<option value=""></option>
<option value="gpt-5.4">gpt-5.4</option>
<option value="gpt-4.1">gpt-4.1</option>
<option value="gpt-4.1-mini">gpt-4.1-mini</option>
{modelOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<label id={anchors.reasoningEffort.split("#")[1]} className="space-y-1 scroll-mt-4">