feat(deskclaw): rebrand + vibe presets + chat model picker
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"gatewayNotRunning": "Gateway Not Running",
|
||||
"gatewayRequired": "The OpenClaw Gateway needs to be running to use chat. It will start automatically, or you can start it from Settings.",
|
||||
"welcome": {
|
||||
"title": "ClawX Chat",
|
||||
"title": "DeskClaw Chat",
|
||||
"subtitle": "What can I do for you?",
|
||||
"askQuestions": "Handle Tasks",
|
||||
"askQuestionsDesc": "Work on task-oriented requests",
|
||||
@@ -19,7 +19,7 @@
|
||||
"eyebrow": "Run View",
|
||||
"title": "Task Outline",
|
||||
"emptyTitle": "No structured steps yet",
|
||||
"emptyBody": "Once a run starts, ClawX will surface thinking, tool calls, and handoff states here.",
|
||||
"emptyBody": "Once a run starts, DeskClaw will surface thinking, tool calls, and handoff states here.",
|
||||
"status": {
|
||||
"idle": "Idle",
|
||||
"running_one": "1 active step",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Settings",
|
||||
"subtitle": "Configure your ClawX experience",
|
||||
"subtitle": "Configure your DeskClaw experience",
|
||||
"appearance": {
|
||||
"title": "General",
|
||||
"description": "Customize the look and feel",
|
||||
@@ -10,7 +10,7 @@
|
||||
"system": "System",
|
||||
"language": "Language",
|
||||
"launchAtStartup": "Launch at system startup",
|
||||
"launchAtStartupDesc": "Automatically launch ClawX when you log in"
|
||||
"launchAtStartupDesc": "Automatically launch DeskClaw when you log in"
|
||||
},
|
||||
"aiProviders": {
|
||||
"title": "AI Providers",
|
||||
@@ -37,7 +37,7 @@
|
||||
"notRequired": "Not required",
|
||||
"empty": {
|
||||
"title": "No providers configured",
|
||||
"desc": "Add an AI provider to start using ClawX",
|
||||
"desc": "Add an AI provider to start using DeskClaw",
|
||||
"cta": "Add Your First Provider"
|
||||
},
|
||||
"dialog": {
|
||||
@@ -60,7 +60,7 @@
|
||||
"protocol": "Protocol",
|
||||
"advancedConfig": "Advanced configuration",
|
||||
"userAgent": "User-Agent",
|
||||
"userAgentPlaceholder": "ClawX/1.0",
|
||||
"userAgentPlaceholder": "DeskClaw/1.0",
|
||||
"fallbackModels": "Fallback Models",
|
||||
"fallbackProviders": "Fallback Providers",
|
||||
"fallbackModelIds": "Fallback Model IDs",
|
||||
@@ -134,7 +134,7 @@
|
||||
"appLogs": "Application Logs",
|
||||
"openFolder": "Open Folder",
|
||||
"autoStart": "Auto-start Gateway",
|
||||
"autoStartDesc": "Start Gateway when ClawX launches",
|
||||
"autoStartDesc": "Start Gateway when DeskClaw launches",
|
||||
"proxyTitle": "Proxy",
|
||||
"proxyDesc": "Route Electron and Gateway traffic through your local proxy client.",
|
||||
"proxyServer": "Proxy Server",
|
||||
@@ -155,7 +155,7 @@
|
||||
},
|
||||
"updates": {
|
||||
"title": "Updates",
|
||||
"description": "Keep ClawX up to date",
|
||||
"description": "Keep DeskClaw up to date",
|
||||
"autoCheck": "Auto-check for updates",
|
||||
"autoCheckDesc": "Check for updates on startup",
|
||||
"autoDownload": "Auto-update",
|
||||
@@ -209,7 +209,7 @@
|
||||
"devMode": "Developer Mode",
|
||||
"devModeDesc": "Show developer tools and shortcuts",
|
||||
"telemetry": "Anonymous Usage Data",
|
||||
"telemetryDesc": "Allow providing anonymous basic usage data to improve ClawX"
|
||||
"telemetryDesc": "Allow providing anonymous basic usage data to improve DeskClaw"
|
||||
},
|
||||
"developer": {
|
||||
"title": "Developer",
|
||||
@@ -271,7 +271,7 @@
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"appName": "ClawX",
|
||||
"appName": "DeskClaw",
|
||||
"tagline": "Graphical AI Assistant",
|
||||
"basedOn": "Based on OpenClaw",
|
||||
"version": "Version {{version}}",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"steps": {
|
||||
"welcome": {
|
||||
"title": "Welcome to ClawX",
|
||||
"title": "Welcome to DeskClaw",
|
||||
"description": "Your AI assistant is ready to be configured"
|
||||
},
|
||||
"runtime": {
|
||||
@@ -22,12 +22,12 @@
|
||||
},
|
||||
"complete": {
|
||||
"title": "All Set!",
|
||||
"description": "ClawX is ready to use"
|
||||
"description": "DeskClaw is ready to use"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Welcome to ClawX",
|
||||
"description": "ClawX is a graphical interface for OpenClaw, making it easy to use AI assistants across your favorite messaging platforms.",
|
||||
"title": "Welcome to DeskClaw",
|
||||
"description": "DeskClaw is a graphical interface for OpenClaw, making it easy to use AI assistants across your favorite messaging platforms.",
|
||||
"features": {
|
||||
"noCommand": "Zero command-line required",
|
||||
"modernUI": "Modern, beautiful interface",
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"complete": {
|
||||
"title": "Setup Complete!",
|
||||
"subtitle": "ClawX is configured and ready to use. You can now start chatting with your AI assistant.",
|
||||
"subtitle": "DeskClaw is configured and ready to use. You can now start chatting with your AI assistant.",
|
||||
"provider": "AI Provider",
|
||||
"components": "Components",
|
||||
"gateway": "Gateway",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"gatewayNotRunning": "ゲートウェイが停止中",
|
||||
"gatewayRequired": "チャットを利用するには OpenClaw ゲートウェイが実行されている必要があります。自動的に起動するか、設定から起動できます。",
|
||||
"welcome": {
|
||||
"title": "ClawX チャット",
|
||||
"title": "DeskClaw チャット",
|
||||
"subtitle": "お手伝いできることはありますか?",
|
||||
"askQuestions": "タスク対応",
|
||||
"askQuestionsDesc": "タスク指向の依頼に対応します",
|
||||
@@ -19,7 +19,7 @@
|
||||
"eyebrow": "実行ビュー",
|
||||
"title": "タスクの流れ",
|
||||
"emptyTitle": "まだ構造化されたステップはありません",
|
||||
"emptyBody": "実行が始まると、ClawX は思考・ツール呼び出し・最終化の状態をここに表示します。",
|
||||
"emptyBody": "実行が始まると、DeskClaw は思考・ツール呼び出し・最終化の状態をここに表示します。",
|
||||
"status": {
|
||||
"idle": "待機中",
|
||||
"running_one": "進行中 1 件",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "設定",
|
||||
"subtitle": "ClawX の体験をカスタマイズ",
|
||||
"subtitle": "DeskClaw の体験をカスタマイズ",
|
||||
"appearance": {
|
||||
"title": "通用",
|
||||
"description": "外観とスタイルをカスタマイズ",
|
||||
@@ -10,7 +10,7 @@
|
||||
"system": "システム",
|
||||
"language": "言語",
|
||||
"launchAtStartup": "システム起動時に自動起動",
|
||||
"launchAtStartupDesc": "ログイン時に ClawX を自動的に起動します"
|
||||
"launchAtStartupDesc": "ログイン時に DeskClaw を自動的に起動します"
|
||||
},
|
||||
"aiProviders": {
|
||||
"title": "AI プロバイダー",
|
||||
@@ -37,7 +37,7 @@
|
||||
"notRequired": "不要",
|
||||
"empty": {
|
||||
"title": "プロバイダーが構成されていません",
|
||||
"desc": "ClawX の使用を開始するには AI プロバイダーを追加してください",
|
||||
"desc": "DeskClaw の使用を開始するには AI プロバイダーを追加してください",
|
||||
"cta": "最初のプロバイダーを追加"
|
||||
},
|
||||
"dialog": {
|
||||
@@ -60,7 +60,7 @@
|
||||
"protocol": "プロトコル",
|
||||
"advancedConfig": "詳細設定",
|
||||
"userAgent": "User-Agent",
|
||||
"userAgentPlaceholder": "ClawX/1.0",
|
||||
"userAgentPlaceholder": "DeskClaw/1.0",
|
||||
"fallbackModels": "フォールバックモデル",
|
||||
"fallbackProviders": "別プロバイダーへのフォールバック",
|
||||
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
|
||||
@@ -133,7 +133,7 @@
|
||||
"appLogs": "アプリケーションログ",
|
||||
"openFolder": "フォルダーを開く",
|
||||
"autoStart": "ゲートウェイ自動起動",
|
||||
"autoStartDesc": "ClawX 起動時にゲートウェイを自動起動",
|
||||
"autoStartDesc": "DeskClaw 起動時にゲートウェイを自動起動",
|
||||
"proxyTitle": "プロキシ",
|
||||
"proxyDesc": "Electron と Gateway の通信をローカルプロキシ経由にします。",
|
||||
"proxyServer": "プロキシサーバー",
|
||||
@@ -154,7 +154,7 @@
|
||||
},
|
||||
"updates": {
|
||||
"title": "アップデート",
|
||||
"description": "ClawX を最新に保つ",
|
||||
"description": "DeskClaw を最新に保つ",
|
||||
"autoCheck": "自動更新チェック",
|
||||
"autoCheckDesc": "起動時に更新を確認",
|
||||
"autoDownload": "自動アップデート",
|
||||
@@ -268,7 +268,7 @@
|
||||
},
|
||||
"about": {
|
||||
"title": "バージョン情報",
|
||||
"appName": "ClawX",
|
||||
"appName": "DeskClaw",
|
||||
"tagline": "グラフィカル AI アシスタント",
|
||||
"basedOn": "OpenClaw ベース",
|
||||
"version": "バージョン {{version}}",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"steps": {
|
||||
"welcome": {
|
||||
"title": "ClawXへようこそ",
|
||||
"title": "DeskClawへようこそ",
|
||||
"description": "AIアシスタントの設定準備が整いました"
|
||||
},
|
||||
"runtime": {
|
||||
@@ -22,12 +22,12 @@
|
||||
},
|
||||
"complete": {
|
||||
"title": "完了!",
|
||||
"description": "ClawXを使用する準備が整いました"
|
||||
"description": "DeskClawを使用する準備が整いました"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"title": "ClawXへようこそ",
|
||||
"description": "ClawXはOpenClawのグラフィカルインターフェースで、お気に入りのメッセージングプラットフォームでAIアシスタントを簡単に使用できます。",
|
||||
"title": "DeskClawへようこそ",
|
||||
"description": "DeskClawはOpenClawのグラフィカルインターフェースで、お気に入りのメッセージングプラットフォームでAIアシスタントを簡単に使用できます。",
|
||||
"features": {
|
||||
"noCommand": "コマンドライン不要",
|
||||
"modernUI": "モダンで美しいインターフェース",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"gatewayNotRunning": "网关未运行",
|
||||
"gatewayRequired": "OpenClaw 网关需要运行才能使用聊天。它将自动启动,或者您可以从设置中启动。",
|
||||
"welcome": {
|
||||
"title": "ClawX 聊天",
|
||||
"title": "DeskClaw 聊天",
|
||||
"subtitle": "我能为你做些什么?",
|
||||
"askQuestions": "处理任务",
|
||||
"askQuestionsDesc": "处理面向任务的请求",
|
||||
@@ -19,7 +19,7 @@
|
||||
"eyebrow": "运行视图",
|
||||
"title": "任务脉络",
|
||||
"emptyTitle": "还没有结构化步骤",
|
||||
"emptyBody": "当一次运行开始后,ClawX 会在这里展示思考、工具调用和收尾状态。",
|
||||
"emptyBody": "当一次运行开始后,DeskClaw 会在这里展示思考、工具调用和收尾状态。",
|
||||
"status": {
|
||||
"idle": "空闲",
|
||||
"running_one": "1 个活动步骤",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "设置",
|
||||
"subtitle": "配置您的 ClawX 体验",
|
||||
"subtitle": "配置您的 DeskClaw 体验",
|
||||
"appearance": {
|
||||
"title": "通用",
|
||||
"description": "自定义外观和风格",
|
||||
@@ -10,7 +10,7 @@
|
||||
"system": "跟随系统",
|
||||
"language": "语言",
|
||||
"launchAtStartup": "开机自动启动",
|
||||
"launchAtStartupDesc": "登录系统后自动启动 ClawX"
|
||||
"launchAtStartupDesc": "登录系统后自动启动 DeskClaw"
|
||||
},
|
||||
"aiProviders": {
|
||||
"title": "AI 模型提供商",
|
||||
@@ -37,7 +37,7 @@
|
||||
"notRequired": "非必填",
|
||||
"empty": {
|
||||
"title": "未配置提供商",
|
||||
"desc": "添加 AI 提供商以开始使用 ClawX",
|
||||
"desc": "添加 AI 提供商以开始使用 DeskClaw",
|
||||
"cta": "添加您的第一个提供商"
|
||||
},
|
||||
"dialog": {
|
||||
@@ -60,7 +60,7 @@
|
||||
"protocol": "协议",
|
||||
"advancedConfig": "高级配置",
|
||||
"userAgent": "User-Agent",
|
||||
"userAgentPlaceholder": "ClawX/1.0",
|
||||
"userAgentPlaceholder": "DeskClaw/1.0",
|
||||
"fallbackModels": "回退模型",
|
||||
"fallbackProviders": "跨 Provider 回退",
|
||||
"fallbackModelIds": "同 Provider 回退模型 ID",
|
||||
@@ -134,7 +134,7 @@
|
||||
"appLogs": "应用日志",
|
||||
"openFolder": "打开文件夹",
|
||||
"autoStart": "自动启动网关",
|
||||
"autoStartDesc": "ClawX 启动时自动启动网关",
|
||||
"autoStartDesc": "DeskClaw 启动时自动启动网关",
|
||||
"proxyTitle": "代理",
|
||||
"proxyDesc": "让 Electron 和 Gateway 的网络请求都走本地代理客户端。",
|
||||
"proxyServer": "代理服务器",
|
||||
@@ -155,7 +155,7 @@
|
||||
},
|
||||
"updates": {
|
||||
"title": "更新",
|
||||
"description": "保持 ClawX 最新",
|
||||
"description": "保持 DeskClaw 最新",
|
||||
"autoCheck": "自动检查更新",
|
||||
"autoCheckDesc": "启动时检查更新",
|
||||
"autoDownload": "自动更新",
|
||||
@@ -209,7 +209,7 @@
|
||||
"devMode": "开发者模式",
|
||||
"devModeDesc": "显示开发者工具和快捷方式",
|
||||
"telemetry": "匿名使用数据",
|
||||
"telemetryDesc": "允许提供匿名的基础使用数据,用于改进 ClawX"
|
||||
"telemetryDesc": "允许提供匿名的基础使用数据,用于改进 DeskClaw"
|
||||
},
|
||||
"developer": {
|
||||
"title": "开发者",
|
||||
@@ -271,7 +271,7 @@
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"appName": "ClawX",
|
||||
"appName": "DeskClaw",
|
||||
"tagline": "图形化 AI 助手",
|
||||
"basedOn": "基于 OpenClaw",
|
||||
"version": "版本 {{version}}",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"steps": {
|
||||
"welcome": {
|
||||
"title": "欢迎使用 ClawX",
|
||||
"title": "欢迎使用 DeskClaw",
|
||||
"description": "您的 AI 助手已准备好进行配置"
|
||||
},
|
||||
"runtime": {
|
||||
@@ -22,12 +22,12 @@
|
||||
},
|
||||
"complete": {
|
||||
"title": "准备就绪!",
|
||||
"description": "ClawX 已准备好使用"
|
||||
"description": "DeskClaw 已准备好使用"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"title": "欢迎使用 ClawX",
|
||||
"description": "ClawX 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。",
|
||||
"title": "欢迎使用 DeskClaw",
|
||||
"description": "DeskClaw 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。",
|
||||
"features": {
|
||||
"noCommand": "无需命令行",
|
||||
"modernUI": "现代美观的界面",
|
||||
|
||||
@@ -87,6 +87,7 @@ function readFileAsBase64(file: globalThis.File): Promise<string> {
|
||||
export function ChatInput({ onSend, onStop, disabled = false, sending = false, isEmpty = false }: ChatInputProps) {
|
||||
const { t } = useTranslation('chat');
|
||||
const [input, setInput] = useState('');
|
||||
const [vibeMode, setVibeMode] = useState<string>('');
|
||||
const [attachments, setAttachments] = useState<FileAttachment[]>([]);
|
||||
const [targetAgentId, setTargetAgentId] = useState<string | null>(null);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
@@ -109,6 +110,31 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
||||
[agents, targetAgentId],
|
||||
);
|
||||
const showAgentPicker = mentionableAgents.length > 0;
|
||||
const vibePresets = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'plan',
|
||||
label: 'Plan',
|
||||
text: 'Mode: Plan.\nAsk missing questions first. Then propose a step-by-step plan and a short checklist. Wait for confirmation before implementing.',
|
||||
},
|
||||
{
|
||||
key: 'build',
|
||||
label: 'Build',
|
||||
text: 'Mode: Build.\nImplement the requested change. Keep it minimal, production-grade, and include verification steps.',
|
||||
},
|
||||
{
|
||||
key: 'debug',
|
||||
label: 'Debug',
|
||||
text: 'Mode: Debug.\nList hypotheses, propose the smallest reproduction, add instrumentation, then fix the root cause.',
|
||||
},
|
||||
{
|
||||
key: 'review',
|
||||
label: 'Review',
|
||||
text: 'Mode: Review.\nReview for correctness, security, and edge cases. Propose concrete fixes.',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
// Auto-resize textarea
|
||||
useEffect(() => {
|
||||
@@ -293,7 +319,11 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
||||
const readyAttachments = attachments.filter(a => a.status === 'ready');
|
||||
// Capture values before clearing — clear input immediately for snappy UX,
|
||||
// but keep attachments available for the async send
|
||||
const textToSend = input.trim();
|
||||
let textToSend = input.trim();
|
||||
const preset = vibeMode ? vibePresets.find((p) => p.key === vibeMode) : null;
|
||||
if (preset) {
|
||||
textToSend = textToSend ? `${preset.text}\n\n---\n\n${textToSend}` : preset.text;
|
||||
}
|
||||
const attachmentsToSend = readyAttachments.length > 0 ? readyAttachments : undefined;
|
||||
console.log(`[handleSend] text="${textToSend.substring(0, 50)}", attachments=${attachments.length}, ready=${readyAttachments.length}, sending=${!!attachmentsToSend}`);
|
||||
if (attachmentsToSend) {
|
||||
@@ -310,7 +340,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
||||
onSend(textToSend, attachmentsToSend, targetAgentId);
|
||||
setTargetAgentId(null);
|
||||
setPickerOpen(false);
|
||||
}, [input, attachments, canSend, onSend, targetAgentId]);
|
||||
}, [input, attachments, canSend, onSend, targetAgentId, vibeMode, vibePresets]);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
if (!canStop) return;
|
||||
@@ -423,6 +453,40 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!disabled && (
|
||||
<div className="flex flex-wrap gap-1 pb-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'rounded-full border px-2.5 py-1 text-[12px] font-medium transition-colors',
|
||||
vibeMode === ''
|
||||
? 'border-primary/30 bg-primary/10 text-primary'
|
||||
: 'border-black/10 bg-black/[0.03] text-muted-foreground hover:bg-black/5 hover:text-foreground dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/[0.06]'
|
||||
)}
|
||||
onClick={() => setVibeMode('')}
|
||||
disabled={sending}
|
||||
>
|
||||
Normal
|
||||
</button>
|
||||
{vibePresets.map((preset) => (
|
||||
<button
|
||||
key={preset.key}
|
||||
type="button"
|
||||
className={cn(
|
||||
'rounded-full border px-2.5 py-1 text-[12px] font-medium transition-colors',
|
||||
vibeMode === preset.key
|
||||
? 'border-primary/30 bg-primary/10 text-primary'
|
||||
: 'border-black/10 bg-black/[0.03] text-muted-foreground hover:bg-black/5 hover:text-foreground dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/[0.06]'
|
||||
)}
|
||||
onClick={() => setVibeMode(preset.key)}
|
||||
disabled={sending}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Row — flush-left */}
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
|
||||
@@ -6,22 +6,118 @@
|
||||
import { useMemo } from 'react';
|
||||
import { RefreshCw, Bot } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select } from '@/components/ui/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import { useAgentsStore } from '@/stores/agents';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import type { ProviderAccount, ProviderVendorInfo, ProviderWithKeyInfo } from '@/stores/providers';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function providerKeyFromAccount(account: ProviderAccount): string {
|
||||
if (account.vendorId === 'custom' || account.vendorId === 'ollama') {
|
||||
const suffix = account.id.replace(/-/g, '').slice(0, 8);
|
||||
return `${account.vendorId}-${suffix}`;
|
||||
}
|
||||
if (account.vendorId === 'minimax-portal-cn') {
|
||||
return 'minimax-portal';
|
||||
}
|
||||
return account.vendorId;
|
||||
}
|
||||
|
||||
function hasConfiguredProviderCredentials(
|
||||
account: ProviderAccount,
|
||||
statusById: Map<string, ProviderWithKeyInfo>,
|
||||
): boolean {
|
||||
if (account.authMode === 'oauth_device' || account.authMode === 'oauth_browser' || account.authMode === 'local') {
|
||||
return true;
|
||||
}
|
||||
return statusById.get(account.id)?.hasKey ?? false;
|
||||
}
|
||||
|
||||
function resolveAccountDefaultModelId(account: ProviderAccount, vendor: ProviderVendorInfo | undefined): string | undefined {
|
||||
const fromAccount = (account.model || '').trim();
|
||||
if (fromAccount) return fromAccount;
|
||||
const fromVendor = (vendor?.defaultModelId || '').trim();
|
||||
return fromVendor || undefined;
|
||||
}
|
||||
|
||||
function splitModelRef(modelRef: string | null | undefined): { providerKey: string; modelId: string } | null {
|
||||
const value = (modelRef || '').trim();
|
||||
if (!value) return null;
|
||||
const separatorIndex = value.indexOf('/');
|
||||
if (separatorIndex <= 0 || separatorIndex >= value.length - 1) return null;
|
||||
return {
|
||||
providerKey: value.slice(0, separatorIndex),
|
||||
modelId: value.slice(separatorIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
export function ChatToolbar() {
|
||||
const refresh = useChatStore((s) => s.refresh);
|
||||
const loading = useChatStore((s) => s.loading);
|
||||
const currentAgentId = useChatStore((s) => s.currentAgentId);
|
||||
const agents = useAgentsStore((s) => s.agents);
|
||||
const defaultModelRef = useAgentsStore((s) => s.defaultModelRef);
|
||||
const updateDefaultModel = useAgentsStore((s) => s.updateDefaultModel);
|
||||
const updateAgentModel = useAgentsStore((s) => s.updateAgentModel);
|
||||
const providerAccounts = useProviderStore((s) => s.accounts);
|
||||
const providerStatuses = useProviderStore((s) => s.statuses);
|
||||
const providerVendors = useProviderStore((s) => s.vendors);
|
||||
const { t } = useTranslation('chat');
|
||||
const currentAgentName = useMemo(
|
||||
() => (agents ?? []).find((agent) => agent.id === currentAgentId)?.name ?? currentAgentId,
|
||||
[agents, currentAgentId],
|
||||
);
|
||||
const currentAgent = useMemo(
|
||||
() => (agents ?? []).find((agent) => agent.id === currentAgentId) ?? null,
|
||||
[agents, currentAgentId],
|
||||
);
|
||||
|
||||
const providerVendorById = useMemo(() => {
|
||||
const map = new Map<string, ProviderVendorInfo>();
|
||||
for (const v of providerVendors) map.set(v.id, v);
|
||||
return map;
|
||||
}, [providerVendors]);
|
||||
|
||||
const providerStatusById = useMemo(() => {
|
||||
const map = new Map<string, ProviderWithKeyInfo>();
|
||||
for (const s of providerStatuses) map.set(s.id, s);
|
||||
return map;
|
||||
}, [providerStatuses]);
|
||||
|
||||
const runtimeModelOptions = useMemo(() => {
|
||||
return (providerAccounts ?? [])
|
||||
.filter((a) => a.enabled)
|
||||
.filter((a) => hasConfiguredProviderCredentials(a, providerStatusById))
|
||||
.map((account) => {
|
||||
const vendor = providerVendorById.get(account.vendorId);
|
||||
const providerKey = providerKeyFromAccount(account);
|
||||
const modelId = resolveAccountDefaultModelId(account, vendor);
|
||||
const modelRef = modelId ? `${providerKey}/${modelId}` : null;
|
||||
const label = `${account.label || providerKey}${modelId ? ` · ${modelId}` : ''}`;
|
||||
return {
|
||||
id: account.id,
|
||||
providerKey,
|
||||
modelId,
|
||||
modelRef,
|
||||
label,
|
||||
};
|
||||
})
|
||||
.filter((o) => Boolean(o.modelRef));
|
||||
}, [providerAccounts, providerStatusById, providerVendorById]);
|
||||
|
||||
const defaultModelSelectValue = useMemo(() => {
|
||||
const parsed = splitModelRef(defaultModelRef);
|
||||
if (!parsed) return '';
|
||||
return defaultModelRef || '';
|
||||
}, [defaultModelRef]);
|
||||
|
||||
const agentOverrideValue = useMemo(() => {
|
||||
const override = currentAgent?.overrideModelRef;
|
||||
return override || '';
|
||||
}, [currentAgent?.overrideModelRef]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -29,6 +125,42 @@ export function ChatToolbar() {
|
||||
<Bot className="h-3.5 w-3.5 text-primary" />
|
||||
<span>{t('toolbar.currentAgent', { agent: currentAgentName })}</span>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Select
|
||||
className="h-8 w-[260px] text-[12px]"
|
||||
value={defaultModelSelectValue}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value.trim();
|
||||
void updateDefaultModel(next ? next : null);
|
||||
}}
|
||||
disabled={runtimeModelOptions.length === 0}
|
||||
title="Default model"
|
||||
>
|
||||
<option value="">{t('toolbar.modelDefaultUnset', { defaultValue: 'Default model (not set)' })}</option>
|
||||
{runtimeModelOptions.map((o) => (
|
||||
<option key={o.id} value={o.modelRef || ''}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
className="h-8 w-[260px] text-[12px]"
|
||||
value={agentOverrideValue}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value.trim();
|
||||
void updateAgentModel(currentAgentId, next ? next : null);
|
||||
}}
|
||||
disabled={!currentAgent || runtimeModelOptions.length === 0}
|
||||
title="Agent override"
|
||||
>
|
||||
<option value="">{t('toolbar.modelInherit', { defaultValue: 'Inherit default' })}</option>
|
||||
{runtimeModelOptions.map((o) => (
|
||||
<option key={o.id} value={o.modelRef || ''}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{/* Refresh */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -1091,14 +1091,14 @@ export function Settings() {
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto p-0 text-[14px] text-blue-500 hover:text-blue-600 font-medium"
|
||||
onClick={() => window.electron.openExternal('https://claw-x.com')}
|
||||
onClick={() => window.electron.openExternal('https://github.rommark.dev/admin/DeskClaw')}
|
||||
>
|
||||
{t('about.docs')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto p-0 text-[14px] text-blue-500 hover:text-blue-600 font-medium"
|
||||
onClick={() => window.electron.openExternal('https://github.com/ValueCell-ai/ClawX')}
|
||||
onClick={() => window.electron.openExternal('https://github.rommark.dev/admin/DeskClaw')}
|
||||
>
|
||||
{t('about.github')}
|
||||
</Button>
|
||||
|
||||
@@ -16,6 +16,7 @@ interface AgentsState {
|
||||
createAgent: (name: string, options?: { inheritWorkspace?: boolean }) => Promise<void>;
|
||||
updateAgent: (agentId: string, name: string) => Promise<void>;
|
||||
updateAgentModel: (agentId: string, modelRef: string | null) => Promise<void>;
|
||||
updateDefaultModel: (modelRef: string | null) => Promise<void>;
|
||||
deleteAgent: (agentId: string) => Promise<void>;
|
||||
assignChannel: (agentId: string, channelType: ChannelType) => Promise<void>;
|
||||
removeChannel: (agentId: string, channelType: ChannelType) => Promise<void>;
|
||||
@@ -104,6 +105,23 @@ export const useAgentsStore = create<AgentsState>((set) => ({
|
||||
}
|
||||
},
|
||||
|
||||
updateDefaultModel: async (modelRef: string | null) => {
|
||||
set({ error: null });
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshot & { success?: boolean }>(
|
||||
'/api/agents/default-model',
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ modelRef }),
|
||||
}
|
||||
);
|
||||
set(applySnapshot(snapshot));
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteAgent: async (agentId: string) => {
|
||||
set({ error: null });
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user