Files
aragrp/components/playground/playground-workspace.tsx
alirez.brandCursor a47ec493c5 feat: add version history drawer with release notes
Introduce a footer-triggered Version History drawer with categorized release notes, hardcoded release data, and semver tracking at v0.3.0. Also ships blog blockquote/table styling, site-wide typography, home page image updates, header refinements, and related bug fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 02:46:20 +03:30

162 lines
4.8 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { ChatPanel } from "./chat-panel";
import { sendMessageToAI } from "./mock-ai";
import { PlaygroundToolsSheet } from "./playground-tools-sheet";
import type { ToolsTab } from "./playground-tools-sheet";
import type { ChatMessage, InteractionLog, PlaygroundSettings } from "./types";
const DEFAULT_SETTINGS: PlaygroundSettings = {
model: "fip-assistant",
responseMode: "balanced",
temperature: 0.7,
memoryEnabled: true,
};
function createMessage(role: ChatMessage["role"], content: string): ChatMessage {
return {
id: `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
role,
content,
timestamp: new Date(),
};
}
const MODEL_LABELS: Record<string, string> = {
"fip-assistant": "دستیار فیپ (آزمایشی)",
"fip-fast": "فیپ سریع",
"fip-pro": "فیپ پیشرفته",
};
export function PlaygroundWorkspace() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputValue, setInputValue] = useState("");
const [isSending, setIsSending] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [settings, setSettings] = useState<PlaygroundSettings>(DEFAULT_SETTINGS);
const [logs, setLogs] = useState<InteractionLog[]>([]);
const [activeScenarioId, setActiveScenarioId] = useState<string | null>(null);
const [toolsOpen, setToolsOpen] = useState(false);
const [toolsTab, setToolsTab] = useState<ToolsTab>("settings");
const bottomRef = useRef<HTMLDivElement>(null);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
if (messages.length === 0) return;
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
}, [messages, isTyping]);
useEffect(() => {
return () => {
abortRef.current?.abort();
};
}, []);
const submitMessage = useCallback(
async (text: string) => {
const trimmed = text.trim();
if (!trimmed || isSending) return;
const userMessage = createMessage("user", trimmed);
const nextMessages = [...messages, userMessage];
setMessages(nextMessages);
setInputValue("");
setIsSending(true);
setIsTyping(true);
setActiveScenarioId(null);
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
const startedAt = Date.now();
try {
const assistantText = await sendMessageToAI(nextMessages, {
settings,
signal: controller.signal,
});
const assistantMessage = createMessage("assistant", assistantText);
setMessages((prev) => [...prev, assistantMessage]);
const log: InteractionLog = {
id: `log-${Date.now()}`,
userMessage: trimmed,
assistantMessage: assistantText,
model: MODEL_LABELS[settings.model] ?? settings.model,
responseMode: settings.responseMode,
timestamp: new Date().toISOString(),
durationMs: Date.now() - startedAt,
};
setLogs((prev) => [...prev, log]);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
const errorMessage = createMessage(
"assistant",
"متأسفانه در پردازش درخواست خطایی رخ داد. لطفاً دوباره تلاش کنید."
);
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsTyping(false);
setIsSending(false);
}
},
[isSending, messages, settings]
);
const handleSubmit = useCallback(() => {
void submitMessage(inputValue);
}, [inputValue, submitMessage]);
function handleStarterSelect(_id: string, prompt: string) {
void submitMessage(prompt);
}
function handleScenarioSelect(scenarioId: string, prompt: string) {
setInputValue(prompt);
setActiveScenarioId(scenarioId);
}
function handleOpenTools(tab: ToolsTab) {
setToolsTab(tab);
setToolsOpen(true);
}
const latestLog = logs.length > 0 ? logs[logs.length - 1]! : null;
return (
<>
<ChatPanel
messages={messages}
inputValue={inputValue}
onInputChange={setInputValue}
onSubmit={handleSubmit}
onStarterSelect={handleStarterSelect}
onOpenTools={handleOpenTools}
isSending={isSending}
isTyping={isTyping}
bottomRef={bottomRef}
/>
<PlaygroundToolsSheet
open={toolsOpen}
onOpenChange={setToolsOpen}
activeTab={toolsTab}
onTabChange={setToolsTab}
settings={settings}
onSettingsChange={setSettings}
latestLog={latestLog}
logs={logs}
onSelectScenario={handleScenarioSelect}
activeScenarioId={activeScenarioId}
/>
</>
);
}