fix: address assistant review feedback
This commit is contained in:
1 parent
d79c748c4d
commit
f67f8c1c33
8 files changed
+178
-160
No files matched your search
@@ -104,6 +104,46 @@ describe("assistant automatic approval tracker", () => {
|
||||
await act(async () => root.unmount());
|
||||
});
|
||||
|
||||
test("does not approve a ready action while switching conversations", async () => {
|
||||
const container = document.createElement("div");
|
||||
containers.push(container);
|
||||
document.body.append(container);
|
||||
const root = createRoot(container);
|
||||
let confirmationCount = 0;
|
||||
const renderConversation = async (conversationId: string, ready: boolean) => {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
createElement(
|
||||
AssistantAutoApprovalProvider,
|
||||
{ conversationId },
|
||||
createElement(
|
||||
Fragment,
|
||||
null,
|
||||
createElement(AutoApprovalProbe),
|
||||
createElement(AutomaticActionProbe, {
|
||||
ready,
|
||||
completed: false,
|
||||
confirm: () => {
|
||||
confirmationCount += 1;
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
await renderConversation("local-thread-1", false);
|
||||
act(() => currentAutoApproval?.setEnabled(true));
|
||||
await renderConversation("local-thread-2", true);
|
||||
|
||||
expect(confirmationCount).toBe(0);
|
||||
expect(currentAutoApproval?.enabled).toBe(false);
|
||||
expect(automaticActionInProgress).toBe(false);
|
||||
|
||||
await act(async () => root.unmount());
|
||||
});
|
||||
|
||||
test("clears claimed calls when automatic approvals are turned off", async () => {
|
||||
const container = document.createElement("div");
|
||||
containers.push(container);
|
||||
|
||||
@@ -59,6 +59,9 @@ export const AssistantAutoApprovalProvider = ({
|
||||
const trackerRef = useRef(createAssistantAutoApprovalTracker(AUTO_APPROVAL_MAX_ATTEMPTS));
|
||||
const retryTimersRef = useRef(new Map<string, { timer: number; mode: AssistantAutomaticActionMode }>());
|
||||
const previousConversationIdRef = useRef(conversationId);
|
||||
const conversationMatches = previousConversationIdRef.current === conversationId;
|
||||
const effectivePreparationEnabled = conversationMatches && preparationEnabled;
|
||||
const effectiveApprovalEnabled = conversationMatches && approvalEnabled;
|
||||
|
||||
const clearRetryTimer = useCallback((toolCallId: string) => {
|
||||
const retry = retryTimersRef.current.get(toolCallId);
|
||||
@@ -115,7 +118,7 @@ export const AssistantAutoApprovalProvider = ({
|
||||
|
||||
const requestAction = useCallback(
|
||||
(toolCallId: string, confirm: () => void, mode: AssistantAutomaticActionMode = "preparation") => {
|
||||
const modeEnabled = mode === "approval" ? approvalEnabled : preparationEnabled;
|
||||
const modeEnabled = mode === "approval" ? effectiveApprovalEnabled : effectivePreparationEnabled;
|
||||
if (!modeEnabled || !trackerRef.current.claim(toolCallId)) return false;
|
||||
|
||||
try {
|
||||
@@ -135,7 +138,7 @@ export const AssistantAutoApprovalProvider = ({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[approvalEnabled, clearRetryTimer, preparationEnabled],
|
||||
[clearRetryTimer, effectiveApprovalEnabled, effectivePreparationEnabled],
|
||||
);
|
||||
|
||||
const completeAction = useCallback(
|
||||
@@ -147,12 +150,12 @@ export const AssistantAutoApprovalProvider = ({
|
||||
);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const enabled = preparationEnabled || approvalEnabled;
|
||||
const enabled = effectivePreparationEnabled || effectiveApprovalEnabled;
|
||||
return {
|
||||
enabled,
|
||||
setEnabled,
|
||||
preparationEnabled,
|
||||
approvalEnabled,
|
||||
preparationEnabled: effectivePreparationEnabled,
|
||||
approvalEnabled: effectiveApprovalEnabled,
|
||||
setPreparationEnabled,
|
||||
setApprovalEnabled,
|
||||
requestAction,
|
||||
@@ -160,9 +163,9 @@ export const AssistantAutoApprovalProvider = ({
|
||||
retryRevision,
|
||||
};
|
||||
}, [
|
||||
approvalEnabled,
|
||||
completeAction,
|
||||
preparationEnabled,
|
||||
effectiveApprovalEnabled,
|
||||
effectivePreparationEnabled,
|
||||
requestAction,
|
||||
retryRevision,
|
||||
setApprovalEnabled,
|
||||
|
||||
@@ -181,23 +181,6 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.windowControls {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.windowControl {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.windowControl:focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.panelAction {
|
||||
width: 2.625rem;
|
||||
height: 2.625rem;
|
||||
@@ -209,10 +192,6 @@
|
||||
padding: 0.45rem 0.5rem 0.45rem 0.65rem;
|
||||
}
|
||||
|
||||
.composerHints {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.historyButton {
|
||||
width: 2.625rem;
|
||||
max-width: 2.625rem;
|
||||
@@ -447,6 +426,9 @@
|
||||
|
||||
.message {
|
||||
margin-block: 0.9rem;
|
||||
}
|
||||
|
||||
.message[data-static] {
|
||||
contain-intrinsic-size: auto 12rem;
|
||||
content-visibility: auto;
|
||||
}
|
||||
@@ -1224,6 +1206,11 @@
|
||||
background: var(--mantine-color-default-hover);
|
||||
}
|
||||
|
||||
.messageMoreItem:focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.messageMoreSeparator {
|
||||
height: 1px;
|
||||
margin-block: 0.2rem;
|
||||
@@ -1554,10 +1541,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.composerHints {
|
||||
padding-inline: 0.45rem;
|
||||
}
|
||||
|
||||
.composerFooter {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
@@ -1826,10 +1809,6 @@
|
||||
padding-inline: 0.1rem;
|
||||
}
|
||||
|
||||
.composerHints {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.runtimeSelectorTrigger {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,6 @@ import {
|
||||
IconFileExport,
|
||||
IconLink,
|
||||
IconMessage,
|
||||
IconMicrophone,
|
||||
IconMinus,
|
||||
IconPaperclip,
|
||||
IconPalette,
|
||||
@@ -1259,7 +1258,7 @@ const UserMessage = () => {
|
||||
const isEditing = useAuiState((state) => state.composer.isEditing);
|
||||
if (isEditing) return <EditComposer />;
|
||||
return (
|
||||
<MessagePrimitive.Root className={`${classes.message} ${classes.userMessageWrap}`}>
|
||||
<MessagePrimitive.Root className={`${classes.message} ${classes.userMessageWrap}`} data-static>
|
||||
<Box className={classes.userMessage}>
|
||||
<MessagePrimitive.Quote>
|
||||
{(quote) => (
|
||||
@@ -1941,51 +1940,58 @@ const WebSearchActivity = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantMessage = () => (
|
||||
<MessagePrimitive.Root className={`${classes.message} ${classes.assistantMessage}`}>
|
||||
<MessagePrimitive.GroupedParts groupBy={assistantMessageGroupBy} indicator="empty">
|
||||
{({ part, children }) => {
|
||||
switch (part.type) {
|
||||
case "group-agent-trace": {
|
||||
const startIndex = part.indices[0];
|
||||
const endIndex = part.indices.at(-1);
|
||||
if (startIndex === undefined || endIndex === undefined) return null;
|
||||
return (
|
||||
<ChainOfThoughtByIndicesProvider startIndex={startIndex} endIndex={endIndex}>
|
||||
<AssistantChainOfThought>{children}</AssistantChainOfThought>
|
||||
</ChainOfThoughtByIndicesProvider>
|
||||
);
|
||||
const AssistantMessage = () => {
|
||||
const isComplete = useAuiState((state) => state.message.status?.type === "complete");
|
||||
|
||||
return (
|
||||
<MessagePrimitive.Root
|
||||
className={`${classes.message} ${classes.assistantMessage}`}
|
||||
data-static={isComplete || undefined}
|
||||
>
|
||||
<MessagePrimitive.GroupedParts groupBy={assistantMessageGroupBy} indicator="empty">
|
||||
{({ part, children }) => {
|
||||
switch (part.type) {
|
||||
case "group-agent-trace": {
|
||||
const startIndex = part.indices[0];
|
||||
const endIndex = part.indices.at(-1);
|
||||
if (startIndex === undefined || endIndex === undefined) return null;
|
||||
return (
|
||||
<ChainOfThoughtByIndicesProvider startIndex={startIndex} endIndex={endIndex}>
|
||||
<AssistantChainOfThought>{children}</AssistantChainOfThought>
|
||||
</ChainOfThoughtByIndicesProvider>
|
||||
);
|
||||
}
|
||||
case "group-reasoning":
|
||||
return <>{children}</>;
|
||||
case "group-tool":
|
||||
return <AgentTraceToolGroup>{children}</AgentTraceToolGroup>;
|
||||
case "indicator":
|
||||
return <AssistantMessagePending status={{ type: "running" }} />;
|
||||
case "text":
|
||||
return <AssistantTextPart />;
|
||||
case "reasoning":
|
||||
return <ReasoningPart {...part} />;
|
||||
case "tool-call":
|
||||
return part.toolUI ?? <ToolPart {...part} />;
|
||||
case "source":
|
||||
return <SourcePart {...part} />;
|
||||
case "file":
|
||||
return <FilePart {...part} />;
|
||||
case "image":
|
||||
return <ImagePart {...part} />;
|
||||
case "data":
|
||||
return part.dataRendererUI;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
case "group-reasoning":
|
||||
return <>{children}</>;
|
||||
case "group-tool":
|
||||
return <AgentTraceToolGroup>{children}</AgentTraceToolGroup>;
|
||||
case "indicator":
|
||||
return <AssistantMessagePending status={{ type: "running" }} />;
|
||||
case "text":
|
||||
return <AssistantTextPart />;
|
||||
case "reasoning":
|
||||
return <ReasoningPart {...part} />;
|
||||
case "tool-call":
|
||||
return part.toolUI ?? <ToolPart {...part} />;
|
||||
case "source":
|
||||
return <SourcePart {...part} />;
|
||||
case "file":
|
||||
return <FilePart {...part} />;
|
||||
case "image":
|
||||
return <ImagePart {...part} />;
|
||||
case "data":
|
||||
return part.dataRendererUI;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
</MessagePrimitive.GroupedParts>
|
||||
<RuntimeError />
|
||||
<WebSearchActivity />
|
||||
<AssistantMessageActions />
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
}}
|
||||
</MessagePrimitive.GroupedParts>
|
||||
<RuntimeError />
|
||||
<WebSearchActivity />
|
||||
<AssistantMessageActions />
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const assistantThreadMessageComponents = { UserMessage, AssistantMessage };
|
||||
|
||||
@@ -2001,7 +2007,13 @@ const ThreadListItem = () => {
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [actionsOpened, setActionsOpened] = useState(false);
|
||||
const [renameOpened, setRenameOpened] = useState(false);
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [nextTitle, setNextTitle] = useState(title ?? "");
|
||||
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (renameOpened) renameInputRef.current?.focus();
|
||||
}, [renameOpened]);
|
||||
|
||||
const exportConversation = async () => {
|
||||
if (!remoteId || exporting) return;
|
||||
@@ -2023,12 +2035,22 @@ const ThreadListItem = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renameConversation = () => {
|
||||
const renameConversation = async () => {
|
||||
const trimmedTitle = nextTitle.trim();
|
||||
if (!trimmedTitle) return;
|
||||
aui.threadListItem().rename(trimmedTitle);
|
||||
setRenameOpened(false);
|
||||
setActionsOpened(false);
|
||||
if (!trimmedTitle || renaming) return;
|
||||
setRenaming(true);
|
||||
try {
|
||||
await aui.threadListItem().rename(trimmedTitle);
|
||||
setRenameOpened(false);
|
||||
setActionsOpened(false);
|
||||
} catch {
|
||||
showErrorNotification({
|
||||
title: t("renameConversation.failedTitle"),
|
||||
message: t("renameConversation.failedDescription"),
|
||||
});
|
||||
} finally {
|
||||
setRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteConversation = () => {
|
||||
@@ -2037,7 +2059,16 @@ const ThreadListItem = () => {
|
||||
children: t("deleteConversation.description", { title: title ?? t("newConversation") }),
|
||||
confirmProps: { color: "red" },
|
||||
labels: { confirm: t("delete"), cancel: t("cancel") },
|
||||
onConfirm: () => aui.threadListItem().delete(),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await aui.threadListItem().delete();
|
||||
} catch {
|
||||
showErrorNotification({
|
||||
title: t("deleteConversation.failedTitle"),
|
||||
message: t("deleteConversation.failedDescription"),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2045,16 +2076,16 @@ const ThreadListItem = () => {
|
||||
return (
|
||||
<ThreadListItemPrimitive.Root className={classes.historyItem}>
|
||||
<TextInput
|
||||
ref={renameInputRef}
|
||||
className={classes.historyRenameInput}
|
||||
value={nextTitle}
|
||||
onChange={(event) => setNextTitle(event.currentTarget.value)}
|
||||
aria-label={t("renameConversation.label")}
|
||||
maxLength={72}
|
||||
size="xs"
|
||||
data-autofocus
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") renameConversation();
|
||||
if (event.key === "Escape") setRenameOpened(false);
|
||||
if (event.key === "Enter") void renameConversation();
|
||||
if (event.key === "Escape" && !renaming) setRenameOpened(false);
|
||||
}}
|
||||
/>
|
||||
<ActionIcon
|
||||
@@ -2063,7 +2094,9 @@ const ThreadListItem = () => {
|
||||
size="sm"
|
||||
aria-label={t("renameConversation.save")}
|
||||
disabled={!nextTitle.trim()}
|
||||
onClick={renameConversation}
|
||||
loading={renaming}
|
||||
loaderProps={{ type: "bars" }}
|
||||
onClick={() => void renameConversation()}
|
||||
>
|
||||
<IconCheck size={15} />
|
||||
</ActionIcon>
|
||||
@@ -2072,6 +2105,7 @@ const ThreadListItem = () => {
|
||||
color="gray"
|
||||
size="sm"
|
||||
aria-label={t("cancel")}
|
||||
disabled={renaming}
|
||||
onClick={() => setRenameOpened(false)}
|
||||
>
|
||||
<IconX size={15} />
|
||||
@@ -2902,26 +2936,6 @@ const Composer = (props: ComposerProps) => {
|
||||
</ActionIcon>
|
||||
</ComposerPrimitive.AddAttachment>
|
||||
</Tooltip>
|
||||
<AuiIf
|
||||
condition={(state) => state.thread.capabilities.dictation && state.composer.dictation === undefined}
|
||||
>
|
||||
<Tooltip label={t("startVoiceInput")}>
|
||||
<ComposerPrimitive.Dictate asChild>
|
||||
<ActionIcon variant="subtle" color="gray" size="lg" aria-label={t("startVoiceInput")}>
|
||||
<IconMicrophone size={17} />
|
||||
</ActionIcon>
|
||||
</ComposerPrimitive.Dictate>
|
||||
</Tooltip>
|
||||
</AuiIf>
|
||||
<AuiIf condition={(state) => state.composer.dictation !== undefined}>
|
||||
<Tooltip label={t("stopVoiceInput")}>
|
||||
<ComposerPrimitive.StopDictation asChild>
|
||||
<ActionIcon variant="light" color="red" size="lg" aria-label={t("stopVoiceInput")}>
|
||||
<IconPlayerStop size={16} />
|
||||
</ActionIcon>
|
||||
</ComposerPrimitive.StopDictation>
|
||||
</Tooltip>
|
||||
</AuiIf>
|
||||
</Group>
|
||||
<LexicalComposerInput
|
||||
ref={composerInputRef}
|
||||
@@ -3334,15 +3348,13 @@ export const AssistantPanel = ({
|
||||
const aui = useAui();
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
||||
const panelRef = useRef<HTMLDialogElement>(null);
|
||||
const stopVoiceActivityWithoutSurface = () => {
|
||||
const stopSpeechWithoutSurface = () => {
|
||||
if (hasVisibleWidget) return;
|
||||
const composer = aui.composer();
|
||||
if (composer.getState().dictation !== undefined) composer.stopDictation();
|
||||
const thread = aui.thread();
|
||||
if (thread.getState().speech !== undefined) thread.stopSpeaking();
|
||||
};
|
||||
const minimize = () => {
|
||||
stopVoiceActivityWithoutSurface();
|
||||
stopSpeechWithoutSurface();
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -3408,7 +3420,7 @@ export const AssistantPanel = ({
|
||||
autoFocusComposer
|
||||
onMinimize={minimize}
|
||||
onDismiss={() => {
|
||||
stopVoiceActivityWithoutSurface();
|
||||
stopSpeechWithoutSurface();
|
||||
onDismissActivity();
|
||||
onClose();
|
||||
}}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type {
|
||||
AttachmentAdapter,
|
||||
DictationAdapter,
|
||||
MessageFormatAdapter,
|
||||
RemoteThreadListAdapter,
|
||||
SpeechSynthesisAdapter,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
useAuiEvent,
|
||||
useAuiState,
|
||||
useRemoteThreadListRuntime,
|
||||
WebSpeechDictationAdapter,
|
||||
WebSpeechSynthesisAdapter,
|
||||
} from "@assistant-ui/react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
@@ -486,10 +484,8 @@ const AssistantThreadRuntime = () => {
|
||||
),
|
||||
[selectedModel],
|
||||
);
|
||||
const [dictation, setDictation] = useState<DictationAdapter>();
|
||||
const [speech, setSpeech] = useState<SpeechSynthesisAdapter>();
|
||||
useEffect(() => {
|
||||
if (WebSpeechDictationAdapter.isSupported()) setDictation(new WebSpeechDictationAdapter());
|
||||
if ("speechSynthesis" in window && typeof SpeechSynthesisUtterance !== "undefined") {
|
||||
setSpeech(new WebSpeechSynthesisAdapter());
|
||||
}
|
||||
@@ -530,7 +526,6 @@ const AssistantThreadRuntime = () => {
|
||||
adapters: {
|
||||
history,
|
||||
attachments,
|
||||
dictation,
|
||||
feedback,
|
||||
speech,
|
||||
},
|
||||
|
||||
@@ -26,7 +26,8 @@ import { z } from "zod/v4";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
|
||||
import {
|
||||
AssistantComposerSurfaceProvider,
|
||||
AssistantComposerRuntimeProvider,
|
||||
AssistantComposerSurfaceBoundary,
|
||||
AssistantRunFocusPreserver,
|
||||
AssistantRuntimeProviderWithTools,
|
||||
assistantSurfaceComposerCacheLimit,
|
||||
@@ -148,14 +149,22 @@ const SurfaceTestAssistant = ({
|
||||
{ runtime, toolkit },
|
||||
createElement(AssistantRunFocusPreserver),
|
||||
createElement(
|
||||
AssistantComposerSurfaceProvider,
|
||||
{ surfaceId: "surface-a" },
|
||||
createElement(SurfaceProbe, { id: "a", onReady }),
|
||||
AssistantComposerRuntimeProvider,
|
||||
null,
|
||||
createElement(
|
||||
AssistantComposerSurfaceBoundary,
|
||||
{ surfaceId: "surface-a" },
|
||||
createElement(SurfaceProbe, { id: "a", onReady }),
|
||||
),
|
||||
),
|
||||
createElement(
|
||||
AssistantComposerSurfaceProvider,
|
||||
{ surfaceId: "surface-b" },
|
||||
createElement(SurfaceProbe, { id: "b", onReady }),
|
||||
AssistantComposerRuntimeProvider,
|
||||
null,
|
||||
createElement(
|
||||
AssistantComposerSurfaceBoundary,
|
||||
{ surfaceId: "surface-b" },
|
||||
createElement(SurfaceProbe, { id: "b", onReady }),
|
||||
),
|
||||
),
|
||||
simulateRunFocus ? createElement(LexicalRunFocusProbe) : null,
|
||||
);
|
||||
|
||||
@@ -42,7 +42,6 @@ class AssistantSurfaceComposerRuntimeCore extends INTERNAL.DefaultThreadComposer
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this.dictation !== undefined) this.stopDictation();
|
||||
composerConnectionDisposers.get(this)?.();
|
||||
composerConnectionDisposers.delete(this);
|
||||
}
|
||||
@@ -158,9 +157,6 @@ const createSurfaceComposerHost = (
|
||||
get attachments() {
|
||||
return getThreadAdapters(threadCore)?.attachments ? attachments : undefined;
|
||||
},
|
||||
get dictation() {
|
||||
return getThreadAdapters(threadCore)?.dictation;
|
||||
},
|
||||
};
|
||||
|
||||
return new Proxy(threadCore as ThreadCoreWithAdapters, {
|
||||
@@ -302,7 +298,6 @@ export const createAssistantSurfaceRuntime = (runtime: AssistantRuntime): Assist
|
||||
const nextCore = sharedBinding.getState();
|
||||
const nextConversationId = getConversationId();
|
||||
if (nextCore !== activeCore || nextConversationId !== activeConversationId) {
|
||||
if (activeComposer.dictation !== undefined) activeComposer.stopDictation();
|
||||
if (activeCore.speech !== undefined) activeCore.stopSpeaking();
|
||||
unsubscribeComposer();
|
||||
releaseConversation(activeConversationId);
|
||||
@@ -365,7 +360,10 @@ export const createAssistantSurfaceRuntime = (runtime: AssistantRuntime): Assist
|
||||
registerModelContextProvider: () => () => {},
|
||||
dispose() {
|
||||
activeConversationCounts.clear();
|
||||
const threadCores = new Set(Array.from(surfaceCoreByConversation.values(), (entry) => entry.source));
|
||||
const threadCores = new Set([
|
||||
...Array.from(surfaceCoreByConversation.values(), (entry) => entry.source),
|
||||
...Array.from(composerByConversation.values(), (entry) => entry.source),
|
||||
]);
|
||||
for (const threadCore of threadCores) {
|
||||
if (threadCore.speech !== undefined) threadCore.stopSpeaking();
|
||||
}
|
||||
@@ -382,7 +380,7 @@ const AssistantComposerSurfaceContext = createContext<AssistantComposerSurfaceVa
|
||||
|
||||
export const useAssistantComposerSurface = () => {
|
||||
const value = useContext(AssistantComposerSurfaceContext);
|
||||
if (!value) throw new Error("useAssistantComposerSurface must be used within AssistantComposerSurfaceProvider");
|
||||
if (!value) throw new Error("useAssistantComposerSurface must be used within AssistantComposerSurfaceBoundary");
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -576,10 +574,6 @@ export const AssistantRunFocusPreserver = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
interface AssistantComposerSurfaceProviderProps extends PropsWithChildren {
|
||||
surfaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives every rendered assistant surface one shared local composer while preserving the shared
|
||||
* conversation. Individual surfaces should add AssistantComposerSurfaceBoundary around their UI.
|
||||
@@ -601,13 +595,3 @@ export const AssistantComposerRuntimeProvider = ({ children }: PropsWithChildren
|
||||
</AssistantRuntimeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience boundary for a standalone surface. Use AssistantComposerRuntimeProvider with
|
||||
* localized boundaries when multiple surfaces need to share one draft and attachment queue.
|
||||
*/
|
||||
export const AssistantComposerSurfaceProvider = ({ children, surfaceId }: AssistantComposerSurfaceProviderProps) => (
|
||||
<AssistantComposerRuntimeProvider>
|
||||
<AssistantComposerSurfaceBoundary surfaceId={surfaceId}>{children}</AssistantComposerSurfaceBoundary>
|
||||
</AssistantComposerRuntimeProvider>
|
||||
);
|
||||
@@ -2309,7 +2309,6 @@
|
||||
"open": "Open Homarr Assistant",
|
||||
"close": "Close assistant",
|
||||
"minimize": "Minimize assistant",
|
||||
"windowControls": "Conversation window controls",
|
||||
"spotlight": "Ask Homarr Assistant",
|
||||
"spotlightDescription": "Type and send a prompt without leaving the command menu.",
|
||||
"spotlightPrompt": {
|
||||
@@ -2334,13 +2333,16 @@
|
||||
"moreActions": "More response actions",
|
||||
"renameConversation": {
|
||||
"action": "Rename",
|
||||
"title": "Rename conversation",
|
||||
"label": "Conversation name",
|
||||
"save": "Save name"
|
||||
"save": "Save name",
|
||||
"failedTitle": "Conversation could not be renamed",
|
||||
"failedDescription": "Homarr could not rename this conversation. Try again."
|
||||
},
|
||||
"deleteConversation": {
|
||||
"title": "Delete conversation?",
|
||||
"description": "“{title}” and its messages will be permanently deleted."
|
||||
"description": "“{title}” and its messages will be permanently deleted.",
|
||||
"failedTitle": "Conversation could not be deleted",
|
||||
"failedDescription": "Homarr could not delete this conversation. Try again."
|
||||
},
|
||||
"closeHistory": "Close conversation history",
|
||||
"openHistory": "Open conversation history",
|
||||
@@ -2416,13 +2418,10 @@
|
||||
"notHelpful": "Not helpful",
|
||||
"readAloud": "Read response aloud",
|
||||
"stopReading": "Stop reading response",
|
||||
"startVoiceInput": "Start voice input",
|
||||
"stopVoiceInput": "Stop voice input",
|
||||
"feedbackError": "Your feedback could not be saved. Try again.",
|
||||
"exportMarkdown": "Export as Markdown",
|
||||
"exportConversation": {
|
||||
"action": "Export conversation as Markdown",
|
||||
"working": "Exporting conversation…",
|
||||
"failedTitle": "Conversation could not be exported",
|
||||
"failedDescription": "Homarr could not load this conversation. Try again."
|
||||
},
|
||||
@@ -2447,7 +2446,6 @@
|
||||
},
|
||||
"mentions": {
|
||||
"menu": "Add Homarr context",
|
||||
"hint": "Type @ to add context",
|
||||
"context": "Context",
|
||||
"app": "Apps",
|
||||
"integration": "Integrations",
|
||||
@@ -2462,7 +2460,6 @@
|
||||
},
|
||||
"commands": {
|
||||
"menu": "Assistant commands",
|
||||
"hint": "Type / for shortcuts",
|
||||
"health": "Prepare a service-health check",
|
||||
"explore": "Prepare an overview of this Homarr instance",
|
||||
"media": "Prepare a media search",
|
||||
@@ -2575,7 +2572,6 @@
|
||||
"otherPlaceholder": "Type another answer…",
|
||||
"submit": "Send answer",
|
||||
"cancelOther": "Cancel other answer",
|
||||
"automaticAffirmative": "Choosing the affirmative answer automatically",
|
||||
"optionKind": {
|
||||
"affirmative": "Confirm",
|
||||
"negative": "Decline",
|
||||
|
||||
Reference in new issue
Block a user