Remove problematic features: wake lock, artifacts panel, keyboard shortcuts, variable substitution
- Remove screen wake lock from chat-adapter.ts (marginal value, doesn't survive tab switches) - Remove artifacts panel, artifact store, and ArtifactEmitter (session-scoped only, hash collision issues, duplicates existing code block actions) - Remove keyboard shortcuts except Cmd/Ctrl+K for command palette (other shortcuts conflict with browser-native bindings on Linux/Windows) - Remove keyboard-shortcut-help.tsx (no longer needed) - Simplify prompt library to copy raw template content instead of using window.prompt() for variable substitution (blocking dialog, broken in some embedded contexts) - Remove shortcut labels from command palette items - Clean up unused imports Kept: search, feedback, export, command palette (Cmd+K), memory, prompt library (without variable substitution), folders/pinning.
This commit is contained in:
parent
1bfcfc9745
commit
940faeaa73
7 changed files with 13 additions and 477 deletions
|
|
@ -16,7 +16,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||
import { Block, type BlockProps, Streamdown } from "streamdown";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { AudioPlayer } from "./audio-player";
|
||||
import { useArtifactStore } from "@/features/chat/stores/artifact-store";
|
||||
|
||||
const math = createMathPlugin({ singleDollarTextMath: true });
|
||||
const { withSmoothContextProvider } = INTERNAL;
|
||||
|
|
@ -337,23 +336,6 @@ function CodeBlockActions({
|
|||
);
|
||||
}
|
||||
|
||||
/** Emits an artifact to the store via useEffect (avoids side-effects in render). */
|
||||
function ArtifactEmitter({ language, source }: { language: string | null; source: string }) {
|
||||
useEffect(() => {
|
||||
const artifactId = `artifact-${hashCode(source)}`;
|
||||
const store = useArtifactStore.getState();
|
||||
if (store.artifacts.some((a) => a.id === artifactId)) return;
|
||||
store.addArtifact({
|
||||
id: artifactId,
|
||||
title: language ? `${language} snippet` : "Code snippet",
|
||||
language,
|
||||
content: source,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
}, [language, source]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function StreamdownBlock(props: BlockProps) {
|
||||
const hasMermaidFence = props.content.includes("```mermaid");
|
||||
const mermaidSource = getMermaidSource(props.content);
|
||||
|
|
@ -401,18 +383,8 @@ function StreamdownBlock(props: BlockProps) {
|
|||
const svgSource = !props.isIncomplete && isSvgFence(codeFence) ? sanitizeSvg(codeFence.source) : null;
|
||||
const htmlSource = !props.isIncomplete && isHtmlFence(codeFence) ? codeFence.source : null;
|
||||
|
||||
// Emit artifact for large code blocks or HTML/SVG/Mermaid
|
||||
const lineCount = codeFence.source.split("\n").length;
|
||||
const isArtifactWorthy =
|
||||
!props.isIncomplete &&
|
||||
(lineCount >= 20 || svgSource !== null || htmlSource !== null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isArtifactWorthy && (
|
||||
<ArtifactEmitter language={codeFence.language} source={codeFence.source} />
|
||||
)}
|
||||
|
||||
<div className="relative isolate">
|
||||
<Block {...props} />
|
||||
<CodeBlockActions
|
||||
|
|
|
|||
|
|
@ -546,16 +546,6 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
return;
|
||||
}
|
||||
|
||||
// Screen wake lock: keep screen on during long generations
|
||||
let wakeLock: WakeLockSentinel | null = null;
|
||||
try {
|
||||
if ("wakeLock" in navigator) {
|
||||
wakeLock = await navigator.wakeLock.request("screen");
|
||||
}
|
||||
} catch {
|
||||
// Wake lock not available or denied -- continue without it
|
||||
}
|
||||
|
||||
const threadKey = unstable_threadId || "__default";
|
||||
let waitingFirstChunk = true;
|
||||
let firstTokenSettled = false;
|
||||
|
|
@ -809,11 +799,6 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
}
|
||||
}
|
||||
runtime.setThreadRunning(threadKey, false);
|
||||
// Release screen wake lock
|
||||
if (wakeLock) {
|
||||
void wakeLock.release().catch(() => {});
|
||||
wakeLock = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandShortcut,
|
||||
} from "@/components/ui/command";
|
||||
import { SidebarProvider, SidebarTrigger, useSidebar } from "@/components/ui/sidebar";
|
||||
import {
|
||||
|
|
@ -39,8 +38,6 @@ import {
|
|||
BookOpenIcon,
|
||||
BrainIcon,
|
||||
ColumnsIcon,
|
||||
KeyboardIcon,
|
||||
PanelRightIcon,
|
||||
PencilIcon,
|
||||
SettingsIcon,
|
||||
} from "lucide-react";
|
||||
|
|
@ -56,9 +53,6 @@ import {
|
|||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { KeyboardShortcutHelp } from "./components/keyboard-shortcut-help";
|
||||
import { ArtifactPanel } from "./components/artifact-panel";
|
||||
import { useArtifactStore } from "./stores/artifact-store";
|
||||
import { PromptLibrarySheet } from "./components/prompt-library-sheet";
|
||||
import { GuidedTour, useGuidedTourController } from "@/features/tour";
|
||||
import { listLocalModels } from "./api/chat-api";
|
||||
|
|
@ -447,9 +441,7 @@ export function ChatPage(): ReactElement {
|
|||
null,
|
||||
);
|
||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
||||
const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false);
|
||||
const [promptLibraryOpen, setPromptLibraryOpen] = useState(false);
|
||||
const artifactPanelOpen = useArtifactStore((s) => s.panelOpen);
|
||||
const inferenceParams = useChatRuntimeStore((state) => state.params);
|
||||
const setInferenceParams = useChatRuntimeStore((state) => state.setParams);
|
||||
const activeGgufVariant = useChatRuntimeStore((state) => state.activeGgufVariant);
|
||||
|
|
@ -530,14 +522,12 @@ export function ChatPage(): ReactElement {
|
|||
const handleNewThread = useCallback(
|
||||
() => {
|
||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||
useArtifactStore.getState().clearArtifacts();
|
||||
setView({ mode: "single", newThreadNonce: crypto.randomUUID() });
|
||||
},
|
||||
[],
|
||||
);
|
||||
const handleNewCompare = useCallback(
|
||||
() => {
|
||||
useArtifactStore.getState().clearArtifacts();
|
||||
setView({ mode: "compare", pairId: crypto.randomUUID() });
|
||||
useChatRuntimeStore.getState().setContextUsage(null);
|
||||
},
|
||||
|
|
@ -566,7 +556,6 @@ export function ChatPage(): ReactElement {
|
|||
const openSidebar = useCallback(() => setSidebarOpen(true), []);
|
||||
|
||||
const enterCompare = useCallback(() => {
|
||||
useArtifactStore.getState().clearArtifacts();
|
||||
setViewBeforeCompare((prev) => prev ?? view);
|
||||
setView({ mode: "compare", pairId: crypto.randomUUID() });
|
||||
useChatRuntimeStore.getState().setContextUsage(null);
|
||||
|
|
@ -574,7 +563,6 @@ export function ChatPage(): ReactElement {
|
|||
|
||||
const exitCompare = useCallback(() => {
|
||||
if (!viewBeforeCompare) return;
|
||||
useArtifactStore.getState().clearArtifacts();
|
||||
setView(viewBeforeCompare);
|
||||
setViewBeforeCompare(null);
|
||||
// Restore context usage from the active thread's last assistant message
|
||||
|
|
@ -596,14 +584,7 @@ export function ChatPage(): ReactElement {
|
|||
|
||||
const handleThreadSelect = useCallback(
|
||||
(nextView: ChatView) => {
|
||||
setView((prev) => {
|
||||
const prevId = prev.mode === "single" ? prev.threadId : prev.pairId;
|
||||
const nextId = nextView.mode === "single" ? nextView.threadId : nextView.pairId;
|
||||
if (prevId !== nextId) {
|
||||
useArtifactStore.getState().clearArtifacts();
|
||||
}
|
||||
return nextView;
|
||||
});
|
||||
setView(nextView);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
|
@ -749,55 +730,22 @@ export function ChatPage(): ReactElement {
|
|||
return () => window.clearTimeout(timeoutId);
|
||||
}, [modelSelectorLocked, tour.open]);
|
||||
|
||||
// Global keyboard shortcuts
|
||||
// Global keyboard shortcut: Cmd/Ctrl+K for command palette
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const mod = e.metaKey || e.ctrlKey;
|
||||
const target = e.target as HTMLElement;
|
||||
const isInput =
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable;
|
||||
|
||||
if (mod && e.key === "k") {
|
||||
e.preventDefault();
|
||||
setCommandPaletteOpen((o) => !o);
|
||||
return;
|
||||
}
|
||||
if (mod && e.shiftKey && e.key === "N") {
|
||||
e.preventDefault();
|
||||
handleNewThread();
|
||||
return;
|
||||
}
|
||||
if (mod && e.shiftKey && e.key === "C") {
|
||||
e.preventDefault();
|
||||
if (canCompare) handleNewCompare();
|
||||
return;
|
||||
}
|
||||
if (mod && e.shiftKey && e.key === "S") {
|
||||
e.preventDefault();
|
||||
setSettingsOpen((o) => !o);
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setCommandPaletteOpen(false);
|
||||
setShortcutHelpOpen(false);
|
||||
return;
|
||||
}
|
||||
if (e.key === "?" && !isInput) {
|
||||
e.preventDefault();
|
||||
setShortcutHelpOpen((o) => !o);
|
||||
return;
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [canCompare, handleNewThread, handleNewCompare]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100dvh-4rem)] bg-background overflow-hidden">
|
||||
<GuidedTour {...tour.tourProps} />
|
||||
<KeyboardShortcutHelp open={shortcutHelpOpen} onOpenChange={setShortcutHelpOpen} />
|
||||
<PromptLibrarySheet
|
||||
open={promptLibraryOpen}
|
||||
onOpenChange={setPromptLibraryOpen}
|
||||
|
|
@ -815,19 +763,16 @@ export function ChatPage(): ReactElement {
|
|||
<CommandItem onSelect={() => { handleNewThread(); setCommandPaletteOpen(false); }}>
|
||||
<PencilIcon className="mr-2 size-4" />
|
||||
New Chat
|
||||
<CommandShortcut>Shift+N</CommandShortcut>
|
||||
</CommandItem>
|
||||
{canCompare && (
|
||||
<CommandItem onSelect={() => { handleNewCompare(); setCommandPaletteOpen(false); }}>
|
||||
<ColumnsIcon className="mr-2 size-4" />
|
||||
Compare Mode
|
||||
<CommandShortcut>Shift+C</CommandShortcut>
|
||||
</CommandItem>
|
||||
)}
|
||||
<CommandItem onSelect={() => { setSettingsOpen(true); setCommandPaletteOpen(false); }}>
|
||||
<SettingsIcon className="mr-2 size-4" />
|
||||
Settings
|
||||
<CommandShortcut>Shift+S</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Actions">
|
||||
|
|
@ -835,22 +780,11 @@ export function ChatPage(): ReactElement {
|
|||
<BookOpenIcon className="mr-2 size-4" />
|
||||
Prompt Library
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => { const s = useArtifactStore.getState(); s.setPanelOpen(!s.panelOpen); setCommandPaletteOpen(false); }}>
|
||||
<PanelRightIcon className="mr-2 size-4" />
|
||||
Toggle Artifacts Panel
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => { handleEject(); setCommandPaletteOpen(false); }}>
|
||||
<BrainIcon className="mr-2 size-4" />
|
||||
Eject Model
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Help">
|
||||
<CommandItem onSelect={() => { setShortcutHelpOpen(true); setCommandPaletteOpen(false); }}>
|
||||
<KeyboardIcon className="mr-2 size-4" />
|
||||
Keyboard Shortcuts
|
||||
<CommandShortcut>?</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</CommandDialog>
|
||||
|
|
@ -943,22 +877,15 @@ export function ChatPage(): ReactElement {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<div className="min-h-0 min-w-0 flex-1">
|
||||
{view.mode === "single" ? (
|
||||
<SingleContent
|
||||
key={view.threadId ?? view.newThreadNonce ?? "new"}
|
||||
threadId={view.threadId}
|
||||
newThreadNonce={view.newThreadNonce}
|
||||
/>
|
||||
) : (
|
||||
<CompareContent key={view.pairId} pairId={view.pairId} models={models} loraModels={loraModels} />
|
||||
)}
|
||||
</div>
|
||||
{artifactPanelOpen && (
|
||||
<div className="hidden w-[24rem] shrink-0 md:block">
|
||||
<ArtifactPanel />
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
{view.mode === "single" ? (
|
||||
<SingleContent
|
||||
key={view.threadId ?? view.newThreadNonce ?? "new"}
|
||||
threadId={view.threadId}
|
||||
newThreadNonce={view.newThreadNonce}
|
||||
/>
|
||||
) : (
|
||||
<CompareContent key={view.pairId} pairId={view.pairId} models={models} loraModels={loraModels} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
||||
import { downloadTextFile } from "@/lib/download";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { type FC, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
type Artifact,
|
||||
useArtifactStore,
|
||||
} from "../stores/artifact-store";
|
||||
|
||||
const COPY_RESET_MS = 2000;
|
||||
|
||||
const ArtifactTab: FC<{ artifact: Artifact; isActive: boolean }> = ({
|
||||
artifact,
|
||||
isActive,
|
||||
}) => {
|
||||
const setActive = useArtifactStore((s) => s.setActiveArtifact);
|
||||
const remove = useArtifactStore((s) => s.removeArtifact);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tab"
|
||||
tabIndex={0}
|
||||
onClick={() => setActive(artifact.id)}
|
||||
onKeyDown={(e) => e.key === "Enter" && setActive(artifact.id)}
|
||||
className={`group flex cursor-pointer items-center gap-1.5 rounded-t-md border-b-2 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
isActive
|
||||
? "border-primary bg-background text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<span className="max-w-24 truncate">{artifact.title}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
remove(artifact.id);
|
||||
}}
|
||||
className="size-4 rounded opacity-0 hover:bg-destructive/20 group-hover:opacity-100"
|
||||
>
|
||||
<XIcon className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ArtifactPanel: FC = () => {
|
||||
const artifacts = useArtifactStore((s) => s.artifacts);
|
||||
const activeId = useArtifactStore((s) => s.activeArtifactId);
|
||||
const panelOpen = useArtifactStore((s) => s.panelOpen);
|
||||
const setPanelOpen = useArtifactStore((s) => s.setPanelOpen);
|
||||
const setVersion = useArtifactStore((s) => s.setActiveVersion);
|
||||
const updateContent = useArtifactStore((s) => s.updateArtifactContent);
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
const resetRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const active = artifacts.find((a) => a.id === activeId) ?? artifacts[0];
|
||||
const viewedContent = active
|
||||
? (active.history[active.activeVersion] ?? active.content)
|
||||
: "";
|
||||
|
||||
// Sync local editor value when switching artifacts or versions
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
setLocalValue(active.history[active.activeVersion] ?? active.content);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [active?.id, active?.activeVersion]);
|
||||
|
||||
// Cleanup copy timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (resetRef.current) clearTimeout(resetRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!panelOpen || artifacts.length === 0 || !active) return null;
|
||||
|
||||
const handleCopy = () => {
|
||||
if (copyToClipboard(localValue)) {
|
||||
setCopied(true);
|
||||
if (resetRef.current) clearTimeout(resetRef.current);
|
||||
resetRef.current = setTimeout(() => setCopied(false), COPY_RESET_MS);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const ext = active.language === "html"
|
||||
? ".html"
|
||||
: active.language === "svg"
|
||||
? ".svg"
|
||||
: active.language
|
||||
? `.${active.language}`
|
||||
: ".txt";
|
||||
downloadTextFile(`${active.title}${ext}`, localValue);
|
||||
};
|
||||
|
||||
const canPrev = active.activeVersion > 0;
|
||||
const canNext = active.activeVersion < active.history.length - 1;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col border-l bg-background">
|
||||
{/* Tab bar */}
|
||||
<div className="flex shrink-0 items-center border-b px-2">
|
||||
<div className="flex flex-1 items-center gap-0.5 overflow-x-auto">
|
||||
{artifacts.map((a) => (
|
||||
<ArtifactTab
|
||||
key={a.id}
|
||||
artifact={a}
|
||||
isActive={a.id === active.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 shrink-0"
|
||||
onClick={() => setPanelOpen(false)}
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex shrink-0 items-center gap-1 border-b px-3 py-1.5">
|
||||
<span className="flex-1 truncate text-xs font-medium">{active.title}</span>
|
||||
{active.history.length > 1 && (
|
||||
<div className="flex items-center gap-0.5 text-xs text-muted-foreground">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canPrev}
|
||||
onClick={() => setVersion(active.id, active.activeVersion - 1)}
|
||||
className="p-0.5 disabled:opacity-30"
|
||||
>
|
||||
<ChevronLeftIcon className="size-3.5" />
|
||||
</button>
|
||||
<span className="tabular-nums">
|
||||
v{active.activeVersion + 1}/{active.history.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canNext}
|
||||
onClick={() => setVersion(active.id, active.activeVersion + 1)}
|
||||
className="p-0.5 disabled:opacity-30"
|
||||
>
|
||||
<ChevronRightIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button type="button" onClick={handleCopy} className="p-1 text-muted-foreground hover:text-foreground">
|
||||
{copied ? <CheckIcon className="size-3.5" /> : <CopyIcon className="size-3.5" />}
|
||||
</button>
|
||||
<button type="button" onClick={handleDownload} className="p-1 text-muted-foreground hover:text-foreground">
|
||||
<DownloadIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (localValue !== viewedContent) {
|
||||
updateContent(active.id, localValue);
|
||||
}
|
||||
}}
|
||||
className="h-full w-full resize-none bg-transparent p-4 font-mono text-xs leading-relaxed outline-none"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { FC } from "react";
|
||||
|
||||
const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
|
||||
const mod = isMac ? "\u2318" : "Ctrl";
|
||||
|
||||
const shortcuts = [
|
||||
{ keys: `${mod}+K`, description: "Open command palette" },
|
||||
{ keys: `${mod}+Shift+N`, description: "New chat" },
|
||||
{ keys: `${mod}+Shift+C`, description: "Toggle compare mode" },
|
||||
{ keys: `${mod}+Shift+S`, description: "Toggle settings" },
|
||||
{ keys: "Escape", description: "Close dialogs / Cancel" },
|
||||
{ keys: "?", description: "Show this help" },
|
||||
];
|
||||
|
||||
export const KeyboardShortcutHelp: FC<{
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}> = ({ open, onOpenChange }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Keyboard Shortcuts</DialogTitle>
|
||||
<DialogDescription>Quick actions for power users</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-1">
|
||||
{shortcuts.map(({ keys, description }) => (
|
||||
<div
|
||||
key={keys}
|
||||
className="flex items-center justify-between py-1.5 text-sm"
|
||||
>
|
||||
<span className="text-muted-foreground">{description}</span>
|
||||
<kbd className="rounded border bg-muted px-2 py-0.5 font-mono text-xs">
|
||||
{keys}
|
||||
</kbd>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -81,15 +81,7 @@ export const PromptLibrarySheet: FC<{
|
|||
|
||||
const handleInsert = useCallback(
|
||||
(p: PromptRecord) => {
|
||||
let result = p.content;
|
||||
if (p.variables.length > 0) {
|
||||
for (const v of p.variables) {
|
||||
const value = window.prompt(`Value for {{${v}}}:`);
|
||||
if (value === null) return; // user cancelled
|
||||
result = result.split(`{{${v}}}`).join(value);
|
||||
}
|
||||
}
|
||||
onInsertPrompt?.(result);
|
||||
onInsertPrompt?.(p.content);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[onInsertPrompt, onOpenChange],
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { create } from "zustand";
|
||||
|
||||
export interface Artifact {
|
||||
id: string;
|
||||
title: string;
|
||||
language: string | null;
|
||||
content: string;
|
||||
/** Version history, most recent last */
|
||||
history: string[];
|
||||
/** Index into history array for current view */
|
||||
activeVersion: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
interface ArtifactStore {
|
||||
artifacts: Artifact[];
|
||||
activeArtifactId: string | null;
|
||||
panelOpen: boolean;
|
||||
|
||||
addArtifact: (artifact: Omit<Artifact, "history" | "activeVersion">) => void;
|
||||
updateArtifactContent: (id: string, content: string) => void;
|
||||
setActiveArtifact: (id: string | null) => void;
|
||||
setActiveVersion: (id: string, version: number) => void;
|
||||
removeArtifact: (id: string) => void;
|
||||
setPanelOpen: (open: boolean) => void;
|
||||
clearArtifacts: () => void;
|
||||
}
|
||||
|
||||
export const useArtifactStore = create<ArtifactStore>((set, get) => ({
|
||||
artifacts: [],
|
||||
activeArtifactId: null,
|
||||
panelOpen: false,
|
||||
|
||||
addArtifact: (artifact) => {
|
||||
const existing = get().artifacts.find((a) => a.id === artifact.id);
|
||||
if (existing) return;
|
||||
set((state) => ({
|
||||
artifacts: [
|
||||
...state.artifacts,
|
||||
{
|
||||
...artifact,
|
||||
history: [artifact.content],
|
||||
activeVersion: 0,
|
||||
},
|
||||
],
|
||||
activeArtifactId: artifact.id,
|
||||
panelOpen: true,
|
||||
}));
|
||||
},
|
||||
|
||||
updateArtifactContent: (id, content) => {
|
||||
set((state) => ({
|
||||
artifacts: state.artifacts.map((a) =>
|
||||
a.id === id
|
||||
? {
|
||||
...a,
|
||||
content,
|
||||
history: [...a.history, content],
|
||||
activeVersion: a.history.length,
|
||||
}
|
||||
: a,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
setActiveArtifact: (id) => {
|
||||
set({ activeArtifactId: id, panelOpen: id !== null });
|
||||
},
|
||||
|
||||
setActiveVersion: (id, version) => {
|
||||
set((state) => ({
|
||||
artifacts: state.artifacts.map((a) =>
|
||||
a.id === id
|
||||
? { ...a, activeVersion: version, content: a.history[version] ?? a.content }
|
||||
: a,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
removeArtifact: (id) => {
|
||||
set((state) => {
|
||||
const filtered = state.artifacts.filter((a) => a.id !== id);
|
||||
return {
|
||||
artifacts: filtered,
|
||||
activeArtifactId:
|
||||
state.activeArtifactId === id
|
||||
? filtered[0]?.id ?? null
|
||||
: state.activeArtifactId,
|
||||
panelOpen: filtered.length > 0,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setPanelOpen: (open) => set({ panelOpen: open }),
|
||||
|
||||
clearArtifacts: () =>
|
||||
set({ artifacts: [], activeArtifactId: null, panelOpen: false }),
|
||||
}));
|
||||
Loading…
Add table
Add a link
Reference in a new issue