From 7bf2b73a522af75acd737e742a4bcc52f2a297d0 Mon Sep 17 00:00:00 2001 From: daniel wang Date: Wed, 9 Sep 2026 09:20:39 +0000 Subject: [PATCH] fix chat bubble and @ --- apps/web/src/App.tsx | 103 ++++++++++++++++++++++++++----- apps/web/src/animated-icons.tsx | 3 + apps/web/src/chat.css | 38 +++++++++--- apps/web/src/locales/en.ts | 5 ++ apps/web/src/locales/zh-TW.ts | 2 + apps/web/src/markdown.tsx | 86 ++++++++++++++++++++++++-- apps/web/src/mentions.ts | 104 ++++++++++++++++++++++++++++++++ apps/web/src/refinements.css | 11 +--- apps/web/src/responsive.css | 2 +- crates/api/src/attachments.rs | 46 +++++++++++--- tests/frontend.test.mjs | 57 ++++++++++++++++- 11 files changed, 409 insertions(+), 48 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index bdba7f1..d39e3ad 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,9 +1,8 @@ -import { FormEvent, Fragment, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { BotIcon, Brain, ChevronDown, ChevronsRight, CircleHelp, ClipboardPaste, Computer, Download, Ellipsis, Info, LogOut, Megaphone, Paperclip, Pencil, Pin, Plug, Plus, RefreshCw, Settings, Smartphone, Sparkle, Square, Upload, Users, X } from "./animated-icons"; +import { FormEvent, Fragment, KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { BotIcon, Brain, ChevronDown, ChevronsRight, CircleHelp, ClipboardPaste, Computer, Download, Ellipsis, Info, LogOut, Megaphone, Paperclip, Pencil, Pin, Plug, Plus, RefreshCw, Reply, Settings, Smartphone, Sparkle, Square, Upload, Users, X } from "./animated-icons"; import UseAnimations from "./use-animations"; import loading from "react-useanimations/lib/loading"; import arrowUp from "react-useanimations/lib/arrowUp"; -import bookmark from "react-useanimations/lib/bookmark"; import copy from "react-useanimations/lib/copy"; import folder from "react-useanimations/lib/folder"; import mail from "react-useanimations/lib/mail"; @@ -17,13 +16,13 @@ import searchToX from "react-useanimations/lib/searchToX"; import { api, ApiError } from "./api"; import { createCoalescer, subscribeToSession } from "./live"; import { applyReplyEvent, type ReplyDrafts } from "./reply-stream"; -import { acceptMention, mentionChoices, mentionToken } from "./mentions"; +import { acceptMention, ensureMention, mentionChoices, mentionToken, replySnippet } from "./mentions"; import { clockTime, dayLabel, sameDay } from "./chat-time"; import { HANDOFF_MS, VEIL_FADE_MS, handoffRemaining, keepScreenUrl, nextVeil, viewOnlyFor, viewerPath, type Veil } from "./handoff"; import { Avatar, AvatarLookProvider, AvatarStack, BLOBATAR_BACKGROUNDS, BLOBATAR_EXPRESSIONS, BLOBATAR_SHAPES, DEFAULT_LOOK, persistBlobatarShape, readAvatarLooks, resolveBlobatarShape, writeAvatarLook, type AvatarBackground, type AvatarExpression, type AvatarLook } from "./avatar"; import { dateLocale, getLocale, listJoin, setLocale, t, useLocale, type MessageKey } from "./i18n"; import type { AvatarShape, Bot, ComputerMode, ComputerStatus, FileSkill, McpCatalogEntry, McpServer, McpTransport, MemoryItem, MemoryStatus, Message, MessageFile, ModelProviderId, Playbook, PlaybookInput, PlaybookStep, Room, RoomMember, Session, TaughtSkill, VoiceSettings, WorkspaceSettings } from "./types"; -import { ChatMarkdown, CopyMessageButton } from "./markdown"; +import { ChatMarkdown, MentionText, copyText } from "./markdown"; import { RunProbe, errorActions, errorTitle } from "./run-monitor"; import { ScheduleEditor, ScheduleList, cronFromPreset, defaultCronPreset, presetFromCron, scheduleWhen, type CronPreset, type ScheduleItem } from "./schedule"; import { CallOverlay, PhoneIcon } from "./call"; @@ -125,6 +124,7 @@ function fileExt(name:string){const dot=name.lastIndexOf(".");const ext=dot>=0?n function messageFiles(blocks:unknown):MessageFile[]{if(!Array.isArray(blocks))return [];return blocks.flatMap(block=>{if(!block||typeof block!=="object")return [];const value=block as {kind?:string;name?:string;mimeType?:string;size?:number};if(value.kind!=="file"&&value.kind!=="image")return [];return [{kind:value.kind,name:value.name||"file",mimeType:value.mimeType,size:value.size}]})} type MessageChip={kind:string;site?:string;why?:string;name?:string;human?:string;cron?:string;reason?:string;turns?:number;limit?:number;code?:string;retryable?:boolean;runId?:string;turn?:number;step?:string|null}; function chipBlocks(blocks:unknown){if(!Array.isArray(blocks))return [] as MessageChip[];return blocks.flatMap(block=>{if(!block||typeof block!=="object")return [];const value=block as MessageChip;if(value.kind==="login"||value.kind==="schedule"||value.kind==="scheduleRun"||value.kind==="resume"||value.kind==="error")return [value];return []})} +function replyQuote(blocks:unknown){if(!Array.isArray(blocks))return null;for(const block of blocks){if(!block||typeof block!=="object")continue;const value=block as {kind?:string;name?:string;body?:string};if(value.kind==="reply")return {name:value.name||"",body:value.body||""}}return null} function resumeTitle(reason?:string){return reason==="budget_exhausted"?t("resumeBudget"):reason==="loop_detected"?t("resumeLoop"):t("resumeMidTask")} function isAutoAttachCaption(body:string,files:MessageFile[]){const text=body.trim();if(!files.length)return false;if(!text)return true;return files.some(file=>text===file.name||text===`附件 ${file.name}`||text===`Attached ${file.name}`||text===t("attachedFile",{name:file.name}))} function isAutoScheduleCaption(body:string,chips:{kind?:string}[]){if(!chips.some(chip=>chip.kind==="schedule"||chip.kind==="scheduleRun"))return false;const text=body.trim();return /^\[排程(試跑)?\]/.test(text)||/^\[Schedule( test)?\]/i.test(text)} @@ -165,6 +165,10 @@ export function App(){ const workspaceStart=readWorkspace(); const [showHidden,setShowHidden]=useState(workspaceStart.showHidden);const[context,setContext]=useState<{bot:Bot;x:number;y:number}|null>(null); const [roomContext,setRoomContext]=useState<{room:Room;x:number;y:number}|null>(null); + const [messageMenu,setMessageMenu]=useState<{message:Message;x:number;y:number}|null>(null); + const [replyTarget,setReplyTarget]=useState(null); + const longPressRef=useRef<{timer:number;x:number;y:number;id:string}|null>(null); + const menuOpenedAtRef=useRef(0); const [roomToDelete,setRoomToDelete]=useState(null); const [mcpServers,setMcpServers]=useState([]); const [accountOpen,setAccountOpen]=useState(false); const [accountDialog,setAccountDialog]=useState(null); const [voiceSettings,setVoiceSettings]=useState(null); const [callOpen,setCallOpen]=useState(false); @@ -284,7 +288,7 @@ export function App(){ useEffect(()=>{if(authRequired)return;api("/api/file-skills").then(setFileSkills).catch(()=>setFileSkills([]))},[authRequired]); useEffect(()=>{if(activeId||activeRoomId||bots.length===0)return;setActiveId(bots[0].id)},[bots,activeId,activeRoomId]); useEffect(()=>{setMessages([]);loadSessions().catch(e=>setError(localizeError(e.message)))},[loadSessions]); - useEffect(()=>{historyIndexRef.current=null;historyDraftRef.current="";setPendingFiles(current=>{current.forEach(file=>file.preview&&URL.revokeObjectURL(file.preview));return []})},[activeSessionId]); + useEffect(()=>{historyIndexRef.current=null;historyDraftRef.current="";setReplyTarget(null);setMessageMenu(null);setPendingFiles(current=>{current.forEach(file=>file.preview&&URL.revokeObjectURL(file.preview));return []})},[activeSessionId]); useEffect(()=>{if(!plusOpen)setSkillQuery("")},[plusOpen]); useEffect(()=>{const panel=messageEndRef.current?.parentElement;if(panel&&panel.scrollHeight-panel.scrollTop-panel.clientHeight<180)panel.scrollTop=panel.scrollHeight},[replyDrafts]); useEffect(()=>{messageEndRef.current?.scrollIntoView({block:"end",behavior:"smooth"})},[activeSessionId,lastMessageId,workingMembers.length,pausedForUser]); @@ -318,8 +322,8 @@ export function App(){ if(!activeRoom||botId===roomHostId)return; try{await api(`/api/rooms/${activeRoom.id}`,{method:"PATCH",body:JSON.stringify({hostBotId:botId})});await loadBots()}catch(error){setError(localizeError(error instanceof Error?error.message:t("operationFailed")))} } - useEffect(()=>{const close=(event:MouseEvent)=>{const target=event.target;if(target instanceof Element&&target.closest(".create-menu-wrap,.account-wrap,.session-picker,.context-menu,.plus-menu-wrap,.host-chip-wrap"))return;setContext(null);setRoomContext(null);setSessionMenuOpen(false);setAccountOpen(false);setCreateMenuOpen(false);setPlusOpen(false);setHostMenuOpen(false)};window.addEventListener("click",close);return()=>window.removeEventListener("click",close)},[]); - useEffect(()=>{const onKey=(event:KeyboardEvent)=>{if(event.key!=="Escape")return;setAccountOpen(false);setAccountDialog(null);setCreateMenuOpen(false);setPlusOpen(false);setTeachOpen(false);setEditingSkillId(null);setSessionMenuOpen(false);setContext(null);setRoomContext(null)};window.addEventListener("keydown",onKey);return()=>window.removeEventListener("keydown",onKey)},[]); + useEffect(()=>{const close=(event:MouseEvent)=>{const target=event.target;if(target instanceof Element&&target.closest(".create-menu-wrap,.account-wrap,.session-picker,.context-menu,.plus-menu-wrap,.host-chip-wrap"))return;if(Date.now()-menuOpenedAtRef.current>=450)setMessageMenu(null);setContext(null);setRoomContext(null);setSessionMenuOpen(false);setAccountOpen(false);setCreateMenuOpen(false);setPlusOpen(false);setHostMenuOpen(false)};window.addEventListener("click",close);return()=>window.removeEventListener("click",close)},[]); + useEffect(()=>{const onKey=(event:KeyboardEvent)=>{if(event.key!=="Escape")return;setAccountOpen(false);setAccountDialog(null);setCreateMenuOpen(false);setPlusOpen(false);setTeachOpen(false);setEditingSkillId(null);setSessionMenuOpen(false);setContext(null);setRoomContext(null);setMessageMenu(null)};window.addEventListener("keydown",onKey);return()=>window.removeEventListener("keydown",onKey)},[]); useEffect(()=>{setWorkspaceName(name=>isDefaultWorkspaceName(name)?t("localWorkspace"):name)},[locale]); useEffect(()=>{localStorage.setItem(WORKSPACE_STORE,JSON.stringify({name:workspaceName,showHidden}))},[workspaceName,showHidden]); useEffect(()=>{if(!sessionStoreKey||!activeSessionId)return;if(!sessions.some(session=>session.id===activeSessionId))return;if(!activeRoomId&&!sessions.some(session=>session.id===activeSessionId&&session.botId===activeId))return;writeSessionStore(sessionStoreKey,activeSessionId)},[sessionStoreKey,activeId,activeRoomId,activeSessionId,sessions]); @@ -347,6 +351,7 @@ export function App(){ finally{setScheduleSaving(false)} } + const mentionAgents=useMemo(()=>activeRoom?activeRoom.members.map(member=>({id:member.id,name:member.name})):[],[activeRoom]); const sessionBusy=workingMembers.length>0; const otherSessionBusy=Boolean(computer.busyBotName&&!sessionBusy); const chatName=activeRoom?.name||active?.name||""; @@ -369,20 +374,22 @@ export function App(){ async function send(event:FormEvent){ event.preventDefault(); if(sendingRef.current||busy)return; - const submittedDraft=draft,text=submittedDraft.trim(),files=pendingFiles,sessionId=activeSessionId; + const submittedDraft=draft,text=submittedDraft.trim(),files=pendingFiles,sessionId=activeSessionId,reply=replyTarget; if((!active&&!activeRoom)||!sessionId||(!text&&files.length===0))return; // An uncertain HTTP result must reuse the original nonce on retry. Keep // the user's input until acknowledgement; a failed refresh is not a failed send. - const key=JSON.stringify([sessionId,text,files.map(file=>file.id)]); + const key=JSON.stringify([sessionId,text,files.map(file=>file.id),reply?.id||""]); const nonce=pendingSendNonces.current.get(key)||clientNonce(); pendingSendNonces.current.set(key,nonce); sendingRef.current=true;setSendingMessage(true); try{await action(async()=>{ const attachments=await Promise.all(files.map(async item=>({name:item.file.name,mimeType:item.file.type,content:await readAsBase64(item.file)}))); - await api(`/api/sessions/${sessionId}/messages`,{method:"POST",body:JSON.stringify({text,clientNonce:nonce,attachments})}); + const quoted=reply?[{kind:"reply",messageId:reply.id,name:reply.role==="user"?t("you"):(reply.speakerName||paneBot?.name||""),body:replySnippet(reply.body)||messageFiles(reply.blocks).map(file=>file.name).join(" · ")}]:[]; + await api(`/api/sessions/${sessionId}/messages`,{method:"POST",body:JSON.stringify({text,clientNonce:nonce,attachments,blocks:quoted})}); pendingSendNonces.current.delete(key); if(currentSessionRef.current===sessionId){ setDraft(current=>current===submittedDraft?"":current); + setReplyTarget(null); setPendingFiles(current=>current.filter(item=>!files.some(sent=>sent.id===item.id))); historyIndexRef.current=null;historyDraftRef.current=""; } @@ -394,6 +401,71 @@ export function App(){ } function pickMention(choice:{name:string}){const next=acceptMention(draft,caretAt,choice.name);setDraft(next.text);setCaretAt(next.caret);requestAnimationFrame(()=>composerRef.current?.focus())} + function openMessageMenu(message:Message,x:number,y:number){ + menuOpenedAtRef.current=Date.now(); + setContext(null);setRoomContext(null);setPlusOpen(false);setSessionMenuOpen(false); + setMessageMenu({message,x,y}); + } + function messagePress(message:Message){ + const skip=(target:EventTarget|null)=>target instanceof Element&&Boolean(target.closest("a[href],img,video,button,input,textarea")); + return { + onContextMenu(event:ReactMouseEvent){ + if(skip(event.target))return; + event.preventDefault(); + event.stopPropagation(); + openMessageMenu(message,event.clientX,event.clientY); + }, + onTouchStart(event:ReactTouchEvent){ + if(skip(event.target))return; + const touch=event.touches[0]; + if(!touch)return; + window.clearTimeout(longPressRef.current?.timer); + const timer=window.setTimeout(()=>{ + longPressRef.current=null; + openMessageMenu(message,touch.clientX,touch.clientY); + try{navigator.vibrate?.(10)}catch{/* ignore */} + },500); + longPressRef.current={timer,x:touch.clientX,y:touch.clientY,id:message.id}; + }, + onTouchMove(event:ReactTouchEvent){ + const press=longPressRef.current; + if(!press||press.id!==message.id)return; + const touch=event.touches[0]; + if(!touch)return; + if(Math.hypot(touch.clientX-press.x,touch.clientY-press.y)>12){ + window.clearTimeout(press.timer); + longPressRef.current=null; + } + }, + onTouchEnd(event:ReactTouchEvent){ + const press=longPressRef.current; + if(press&&press.id===message.id){ + window.clearTimeout(press.timer); + longPressRef.current=null; + } + if(Date.now()-menuOpenedAtRef.current<500)event.preventDefault(); + }, + onTouchCancel(){ + const press=longPressRef.current; + if(!press||press.id!==message.id)return; + window.clearTimeout(press.timer); + longPressRef.current=null; + }, + }; + } + function copyChatMessage(message:Message){ + const selected=window.getSelection()?.toString().trim(); + const text=selected||message.body.trim()||messageFiles(message.blocks).map(file=>file.name).join("\n"); + if(text)void copyText(text); + } + function beginReply(message:Message){ + setMessageMenu(null); + if(message.role==="user")return; + setReplyTarget(message); + const name=message.role!=="user"?(message.speakerName||(activeRoom?paneBot?.name:undefined)):undefined; + if(activeRoom&&name)setDraft(current=>ensureMention(current,name)); + requestAnimationFrame(()=>composerRef.current?.focus()); + } function composerKeyDown(event:ReactKeyboardEvent){if(event.nativeEvent.isComposing||event.key==="Process")return;if(mentionList.length){if(event.key==="Escape"){event.preventDefault();setMentionDismissed(true);return}if(event.key==="ArrowDown"||event.key==="ArrowUp"){event.preventDefault();setMentionIndex(index=>(index+(event.key==="ArrowDown"?1:mentionList.length-1))%mentionList.length);return}if(event.key==="Tab"||(event.key==="Enter"&&!event.shiftKey)){event.preventDefault();pickMention(mentionList[mentionIndex%mentionList.length]);return}}if(slashSuggestions.length){if(event.key==="Escape"){event.preventDefault();setSlashDismissed(true);return}if(event.key==="ArrowDown"||event.key==="ArrowUp"){event.preventDefault();setSlashIndex(index=>(index+(event.key==="ArrowDown"?1:slashSuggestions.length-1))%slashSuggestions.length);return}if(event.key==="Tab"||(event.key==="Enter"&&!event.shiftKey)){event.preventDefault();setDraft(`/${slashSuggestions[slashIndex%slashSuggestions.length].name} `);return}}const history=sentHistoryRef.current;if(event.key==="ArrowUp"&&history.length>0&&(!event.currentTarget.value.includes("\n")||event.currentTarget.selectionStart===0)){event.preventDefault();if(historyIndexRef.current===null){historyDraftRef.current=draft;historyIndexRef.current=history.length-1}else historyIndexRef.current=Math.max(0,historyIndexRef.current-1);setDraft(history[historyIndexRef.current]);return}if(event.key==="ArrowDown"&&historyIndexRef.current!==null&&(!event.currentTarget.value.includes("\n")||event.currentTarget.selectionEnd===event.currentTarget.value.length)){event.preventDefault();if(historyIndexRef.current(sessionsPath,{method:"POST",body:JSON.stringify({title:t("newConversation")})});writeSessionStore(sessionStoreKey,session.id);const next=await api(sessionsPath);setSessions(next);setActiveSessionId(session.id);setMessages([])}catch(e){setError(e instanceof Error?localizeError(e.message):t("operationFailed"))}finally{setBusy(false)}} @@ -435,6 +507,7 @@ export function App(){ async function copyClipboard(){try{await navigator.clipboard.writeText(desktopClipboard)}catch{setError(t("clipboardWriteBlocked"))}} async function inbox(bot:Bot,actionName:string,groupName?:string|null){await api(`/api/bots/${bot.id}/inbox`,{method:"POST",body:JSON.stringify({action:actionName,groupName})});await loadBots()} function openBot(bot:Bot){setCallOpen(false);setMobileNav(false);setSessionMenuOpen(false);if(bot.unreadCount>0)void inbox(bot,"read");if(bot.id===activeId&&!activeRoomId){if(!activeSessionId){const stored=readSessionStore()[bot.id];if(stored)setActiveSessionId(stored);else void loadSessions()}return}setMessages([]);setActiveSessionId(null);setActiveRoomId(null);setBusyMembers([]);setActiveId(bot.id)} + function openMention(id:string){const bot=bots.find(item=>item.id===id);if(!bot)return;setMessageMenu(null);openBot(bot)} function openRoom(room:Room){setCallOpen(false);setMobileNav(false);setSessionMenuOpen(false);if(rightPart==="settings")setRightPart("computer");if(room.id===activeRoomId){if(!activeSessionId){const stored=readSessionStore()[`room:${room.id}`];if(stored)setActiveSessionId(stored);else void loadSessions()}return}setMessages([]);setActiveSessionId(null);setActiveId(null);setBusyMembers([]);setActiveRoomId(room.id)} async function deleteRoom(room:Room){setRoomToDelete(null);await action(async()=>{await api(`/api/rooms/${room.id}`,{method:"DELETE"});if(activeRoomId===room.id){setActiveRoomId(null);setActiveSessionId(null);setMessages([]);setBusyMembers([])}await loadBots()})} function openAccount(dialog:AccountDialog){setAccountOpen(false);setAccountDialog(dialog)} @@ -556,12 +629,12 @@ export function App(){
{activeRoom?<>member.id)}/>{activeRoom.name}event.stopPropagation()}>{hostMenuOpen&&
{t("hostMenuLabel")}{activeRoom.members.map(member=>)}
}
{setSessionMenuOpen(false);setSessionToDelete(id)}} onClear={()=>{setSessionMenuOpen(false);setClearOpen(true)}}/>{topTools}:active?<>{active.name}{setSessionMenuOpen(false);setSessionToDelete(id)}} onClear={()=>{setSessionMenuOpen(false);setClearOpen(true)}}/>{topTools}:<>{t("chooseBot")}{topTools}}
{callOpen&&voiceSettings?.enabled&&active&&activeSessionId&&!activeRoomId?setCallOpen(false)} onTakeOver={()=>{setRightPart("computer");setRightCollapsed(false);setComputerOpen(true);if(active&&!computer.sharedInput)void setControl("user",active.id)}}/>:null} -
{(activeRoom||active)&&messages.length===0?
{activeRoom?:}

{activeRoom?t("startRoomDiscussion",{name:activeRoom.name}):t("startBotWork",{name:active!.name})}

{activeRoom?t("roomWillReply",{names:listJoin(activeRoom.members.map(member=>member.name))}):active!.description||t("botWelcome")}

:messages.map((message,index)=>{const spoken=message.role!=="user"&&Boolean(activeRoom);const speakerName=message.speakerName||(spoken?paneBot?.name:undefined);const speakerShape=(message.speakerShape||paneBot?.avatarShape||"blob") as AvatarShape;const files=messageFiles(message.blocks);const chips=chipBlocks(message.blocks);const hideBody=isAutoAttachCaption(message.body,files)||isAutoScheduleCaption(message.body,chips);const previous=index>0?messages[index-1]:null;const newDay=!previous||!sameDay(previous.createdAt,message.createdAt);return {newDay&&}
{spoken&&}{spoken&&{speakerName}}{files.length>0&&
{files.map(file=>)}
}{chips.map((chip,index)=>chip.kind==="error"?
{errorTitle(chip.code)}
{errorActions(chip.code).map(next=>next==="retry"?:next==="screen"?:)}
:chip.kind==="resume"?
{resumeTitle(chip.reason)}
{(chip.limit||0)>0?
{t("resumeProgress",{turns:chip.turns||0,limit:chip.limit||0})}
:null}{resumedChips[message.id]||(message.seq??0)
}
:chip.kind==="login"?
{t("loginNeedsYou")}
{chip.site||message.body}
{chip.why?
{t("loginWhy",{why:chip.why})}
:null}
:chip.kind==="schedule"?
{t("scheduleChip")}
{chip.name}{scheduleWhen(chip.cron,chip.human)}
:
{t("scheduleRunChip")}
{chip.name}{scheduleWhen(chip.cron,chip.human)}
)}{!hideBody&&(message.role==="assistant"?
{message.body}
{message.body.trim()?:null}{message.runId&&{t("messageRunDetails")}}
:{message.body})}{!hideBody&&message.body.trim()&&}{message.replyBots&&message.replyBots.length>0&&
{message.role==="user"?t("routedTo",{names:listJoin(message.replyBots.map(reply=>reply.name))}):t("handedTo",{name:message.replyBots[0].name})}
}
})}{pausedForUser&&paneBot&&
{computer.sharedInput?t("sharedNeedsUser",{name:paneBot.name}):computer.controlHolder==="user"?t("pausedUserControl",{name:paneBot.name}):t("pausedNeedsUser",{name:paneBot.name})}{(computer.queuedRuns||0)>0&&<> {t("queuedMessages",{count:computer.queuedRuns||0})}}{computer.sharedInput||computer.controlHolder==="user"?:}
}{teaching&&active&&
{t("teachingLive",{goal:teaching.goal})}{t("teachingHint")}{teaching.eventCount>0&&<> · {t("teachingCaptured",{count:teaching.eventCount})}}
}{drafting&&
{t("distilling",{goal:drafting.goal})}
}{skillDraft&&active&&void saveSkill(skillDraft,name,playbook)} onTest={(name,playbook)=>void testSkill(skillDraft,name,playbook)} onDiscard={()=>void discardSkill(skillDraft)} onEdit={()=>setEditingSkillId(skillDraft.id)} onExport={(name,playbook)=>downloadSkill(name,skillDraft.goal,playbook)}/>}{Object.values(replyDrafts).filter(reply=>reply.text&&(!reply.messageId||!messages.some(message=>message.id===reply.messageId))).map(reply=>{const speaker=bots.find(bot=>bot.id===reply.botId);return
{activeRoom&&speaker&&<>{speaker.name}}
{reply.text}
})} +
{(activeRoom||active)&&messages.length===0?
{activeRoom?:}

{activeRoom?t("startRoomDiscussion",{name:activeRoom.name}):t("startBotWork",{name:active!.name})}

{activeRoom?t("roomWillReply",{names:listJoin(activeRoom.members.map(member=>member.name))}):active!.description||t("botWelcome")}

:messages.map((message,index)=>{const spoken=message.role!=="user"&&Boolean(activeRoom);const speakerName=message.speakerName||(spoken?paneBot?.name:undefined);const speakerShape=(message.speakerShape||paneBot?.avatarShape||"blob") as AvatarShape;const files=messageFiles(message.blocks);const chips=chipBlocks(message.blocks);const hideBody=isAutoAttachCaption(message.body,files)||isAutoScheduleCaption(message.body,chips);const quoted=replyQuote(message.blocks);const quoteChip=quoted?{quoted.name||t("you")}:null;const previous=index>0?messages[index-1]:null;const newDay=!previous||!sameDay(previous.createdAt,message.createdAt);return {newDay&&}
{spoken&&}{spoken&&{speakerName}}{files.length>0&&
{files.map(file=>)}
}{chips.map((chip,index)=>chip.kind==="error"?
{errorTitle(chip.code)}
{errorActions(chip.code).map(next=>next==="retry"?:next==="screen"?:)}
:chip.kind==="resume"?
{resumeTitle(chip.reason)}
{(chip.limit||0)>0?
{t("resumeProgress",{turns:chip.turns||0,limit:chip.limit||0})}
:null}{resumedChips[message.id]||(message.seq??0)
}
:chip.kind==="login"?
{t("loginNeedsYou")}
{chip.site||message.body}
{chip.why?
{t("loginWhy",{why:chip.why})}
:null}
:chip.kind==="schedule"?
{t("scheduleChip")}
{chip.name}{scheduleWhen(chip.cron,chip.human)}
:
{t("scheduleRunChip")}
{chip.name}{scheduleWhen(chip.cron,chip.human)}
)}{!hideBody&&(message.role==="assistant"?
{quoteChip}{message.body}
:{quoteChip})}
})}{pausedForUser&&paneBot&&
{computer.sharedInput?t("sharedNeedsUser",{name:paneBot.name}):computer.controlHolder==="user"?t("pausedUserControl",{name:paneBot.name}):t("pausedNeedsUser",{name:paneBot.name})}{(computer.queuedRuns||0)>0&&<> {t("queuedMessages",{count:computer.queuedRuns||0})}}{computer.sharedInput||computer.controlHolder==="user"?:}
}{teaching&&active&&
{t("teachingLive",{goal:teaching.goal})}{t("teachingHint")}{teaching.eventCount>0&&<> · {t("teachingCaptured",{count:teaching.eventCount})}}
}{drafting&&
{t("distilling",{goal:drafting.goal})}
}{skillDraft&&active&&void saveSkill(skillDraft,name,playbook)} onTest={(name,playbook)=>void testSkill(skillDraft,name,playbook)} onDiscard={()=>void discardSkill(skillDraft)} onEdit={()=>setEditingSkillId(skillDraft.id)} onExport={(name,playbook)=>downloadSkill(name,skillDraft.goal,playbook)}/>}{Object.values(replyDrafts).filter(reply=>reply.text&&(!reply.messageId||!messages.some(message=>message.id===reply.messageId))).map(reply=>{const speaker=bots.find(bot=>bot.id===reply.botId);return
{activeRoom&&speaker&&<>{speaker.name}}
{reply.text}
})}
{error&&
{error}
} {otherSessionBusy&&
{t("anotherConversationQueued")}
} - {statusMembers.map(member=>{const step=computer.busyStep&&member.id===computer.botId?computer.busyStep:null;const transition=isTransitionStep(step);const label=t("working",{name:member.name});return
{label}{!transition&&step?{localizeStep(step)}:null}
})} -
{event.preventDefault()}} onDrop={event=>{event.preventDefault();if(event.dataTransfer.files.length)addPendingFiles(event.dataTransfer.files)}}>
event.stopPropagation()}>{plusOpen&&
{savedSkills.length>0&&<>
{t("taughtSkills")}{savedSkills.length>5?` · ${savedSkills.length}`:""}{savedSkills.length>=6&&setSkillQuery(e.target.value)} placeholder={t("searchSkills")} aria-label={t("searchSkills")} onClick={e=>e.stopPropagation()}/>}
{listedSkills.map(skill=>
)}{listedSkills.length===0&&{t("noMatchingSkills")}}
}
}
{const file=event.target.files?.[0];event.currentTarget.value="";if(file)void importSkillFile(file)}}/>{const files=[...event.target.files||[]];event.currentTarget.value="";if(files.length)addPendingFiles(files)}}/>{pendingFiles.length>0&&
{pendingFiles.map(item=>removePendingFile(item.id)}/>)}
}{slashSuggestions.length>0&&
{slashSuggestions.map((skill,index)=>)}
}{mentionList.length>0&&
{mentionList.map((choice,index)=>)}
}