diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx
index 828212055c..b7bfd76b22 100644
--- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx
+++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx
@@ -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 && (
-
- )}
-
{});
- wakeLock = null;
- }
}
},
};
diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx
index e844e97da8..25008a73e5 100644
--- a/studio/frontend/src/features/chat/chat-page.tsx
+++ b/studio/frontend/src/features/chat/chat-page.tsx
@@ -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 (
-
{ handleNewThread(); setCommandPaletteOpen(false); }}>
New Chat
- Shift+N
{canCompare && (
{ handleNewCompare(); setCommandPaletteOpen(false); }}>
Compare Mode
- Shift+C
)}
{ setSettingsOpen(true); setCommandPaletteOpen(false); }}>
Settings
- Shift+S
@@ -835,22 +780,11 @@ export function ChatPage(): ReactElement {
Prompt Library
- { const s = useArtifactStore.getState(); s.setPanelOpen(!s.panelOpen); setCommandPaletteOpen(false); }}>
-
- Toggle Artifacts Panel
-
{ handleEject(); setCommandPaletteOpen(false); }}>
Eject Model
-
- { setShortcutHelpOpen(true); setCommandPaletteOpen(false); }}>
-
- Keyboard Shortcuts
- ?
-
-
@@ -943,22 +877,15 @@ export function ChatPage(): ReactElement {
-
-
- {view.mode === "single" ? (
-
- ) : (
-
- )}
-
- {artifactPanelOpen && (
-
+
+ {view.mode === "single" ? (
+
+ ) : (
+
)}
diff --git a/studio/frontend/src/features/chat/components/artifact-panel.tsx b/studio/frontend/src/features/chat/components/artifact-panel.tsx
deleted file mode 100644
index 3232613aba..0000000000
--- a/studio/frontend/src/features/chat/components/artifact-panel.tsx
+++ /dev/null
@@ -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 (
- 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"
- }`}
- >
- {artifact.title}
-
-
- );
-};
-
-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 | null>(null);
- const textareaRef = useRef(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 (
-
- {/* Tab bar */}
-
-
- {artifacts.map((a) => (
-
- ))}
-
-
-
-
- {/* Toolbar */}
-
-
{active.title}
- {active.history.length > 1 && (
-
-
-
- v{active.activeVersion + 1}/{active.history.length}
-
-
-
- )}
-
-
-
-
- {/* Editor */}
-
-
-
- );
-};
diff --git a/studio/frontend/src/features/chat/components/keyboard-shortcut-help.tsx b/studio/frontend/src/features/chat/components/keyboard-shortcut-help.tsx
deleted file mode 100644
index f1771927ba..0000000000
--- a/studio/frontend/src/features/chat/components/keyboard-shortcut-help.tsx
+++ /dev/null
@@ -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 (
-
- );
-};
diff --git a/studio/frontend/src/features/chat/components/prompt-library-sheet.tsx b/studio/frontend/src/features/chat/components/prompt-library-sheet.tsx
index 42bd2ef40c..d92c0b9d0c 100644
--- a/studio/frontend/src/features/chat/components/prompt-library-sheet.tsx
+++ b/studio/frontend/src/features/chat/components/prompt-library-sheet.tsx
@@ -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],
diff --git a/studio/frontend/src/features/chat/stores/artifact-store.ts b/studio/frontend/src/features/chat/stores/artifact-store.ts
deleted file mode 100644
index 795f4c7dc2..0000000000
--- a/studio/frontend/src/features/chat/stores/artifact-store.ts
+++ /dev/null
@@ -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) => 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((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 }),
-}));