Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 28 additions & 24 deletions src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
{
"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",
"subtitle": "Your AI assistant is ready. Start a conversation below.",
"askQuestions": "Ask Questions",
"askQuestionsDesc": "Get answers on any topic",
"creativeTasks": "Creative Tasks",
"creativeTasksDesc": "Writing, brainstorming, ideas"
},
"noLogs": "(No logs available yet)",
"toolbar": {
"refresh": "Refresh chat",
"showThinking": "Show thinking",
"hideThinking": "Hide thinking"
},
"historyBuckets": {
"today": "Today",
"yesterday": "Yesterday",
"withinWeek": "Within 1 Week",
"withinTwoWeeks": "Within 2 Weeks",
"withinMonth": "Within 1 Month",
"older": "Older than 1 Month"
}
"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",
"subtitle": "Your AI assistant is ready. Start a conversation below.",
"askQuestions": "Ask Questions",
"askQuestionsDesc": "Get answers on any topic",
"creativeTasks": "Creative Tasks",
"creativeTasksDesc": "Writing, brainstorming, ideas"
},
"noLogs": "(No logs available yet)",
"toolbar": {
"refresh": "Refresh chat",
"showThinking": "Show thinking",
"hideThinking": "Hide thinking",
"sessionModel": "Session model",
"sessionModelDefault": "Use agent default model",
"sessionModelLoading": "Loading models...",
"sessionModelUpdateFailed": "Failed to change session model"
},
"historyBuckets": {
"today": "Today",
"yesterday": "Yesterday",
"withinWeek": "Within 1 Week",
"withinTwoWeeks": "Within 2 Weeks",
"withinMonth": "Within 1 Month",
"older": "Older than 1 Month"
}
}
52 changes: 28 additions & 24 deletions src/i18n/locales/ja/chat.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
{
"gatewayNotRunning": "ゲートウェイが停止中",
"gatewayRequired": "チャットを使用するには OpenClaw ゲートウェイが実行されている必要があります。自動的に起動するか、設定から起動できます。",
"welcome": {
"title": "ClawX チャット",
"subtitle": "AI アシスタントの準備ができました。下の入力欄から会話を始めましょう。",
"askQuestions": "質問する",
"askQuestionsDesc": "あらゆるトピックについて回答を得る",
"creativeTasks": "クリエイティブタスク",
"creativeTasksDesc": "ライティング、ブレスト、アイデア"
},
"noLogs": "(ログはまだありません)",
"toolbar": {
"refresh": "チャットを更新",
"showThinking": "思考を表示",
"hideThinking": "思考を非表示"
},
"historyBuckets": {
"today": "今日",
"yesterday": "昨日",
"withinWeek": "1週間以内",
"withinTwoWeeks": "2週間以内",
"withinMonth": "1か月以内",
"older": "1か月より前"
}
"gatewayNotRunning": "ゲートウェイが停止中",
"gatewayRequired": "チャットを使用するには OpenClaw ゲートウェイが実行されている必要があります。自動的に起動するか、設定から起動できます。",
"welcome": {
"title": "ClawX チャット",
"subtitle": "AI アシスタントの準備ができました。下の入力欄から会話を始めましょう。",
"askQuestions": "質問する",
"askQuestionsDesc": "あらゆるトピックについて回答を得る",
"creativeTasks": "クリエイティブタスク",
"creativeTasksDesc": "ライティング、ブレスト、アイデア"
},
"noLogs": "(ログはまだありません)",
"toolbar": {
"refresh": "チャットを更新",
"showThinking": "思考を表示",
"hideThinking": "思考を非表示",
"sessionModel": "セッションモデル",
"sessionModelDefault": "エージェントのデフォルトモデルを使用",
"sessionModelLoading": "モデルを読み込み中...",
"sessionModelUpdateFailed": "セッションモデルの切り替えに失敗しました"
},
"historyBuckets": {
"today": "今日",
"yesterday": "昨日",
"withinWeek": "1週間以内",
"withinTwoWeeks": "2週間以内",
"withinMonth": "1か月以内",
"older": "1か月より前"
}
}
52 changes: 28 additions & 24 deletions src/i18n/locales/zh/chat.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
{
"gatewayNotRunning": "网关未运行",
"gatewayRequired": "OpenClaw 网关需要运行才能使用聊天。它将自动启动,或者您可以从设置中启动。",
"welcome": {
"title": "ClawX 聊天",
"subtitle": "您的 AI 助手已就绪。在下方开始对话。",
"askQuestions": "提问",
"askQuestionsDesc": "获取任何话题的答案",
"creativeTasks": "创意任务",
"creativeTasksDesc": "写作、头脑风暴、创意"
},
"noLogs": "(暂无日志)",
"toolbar": {
"refresh": "刷新聊天",
"showThinking": "显示思考过程",
"hideThinking": "隐藏思考过程"
},
"historyBuckets": {
"today": "今天",
"yesterday": "昨天",
"withinWeek": "一周内",
"withinTwoWeeks": "两周内",
"withinMonth": "一个月内",
"older": "一个月之前"
}
"gatewayNotRunning": "网关未运行",
"gatewayRequired": "OpenClaw 网关需要运行才能使用聊天。它将自动启动,或者您可以从设置中启动。",
"welcome": {
"title": "ClawX 聊天",
"subtitle": "您的 AI 助手已就绪。在下方开始对话。",
"askQuestions": "提问",
"askQuestionsDesc": "获取任何话题的答案",
"creativeTasks": "创意任务",
"creativeTasksDesc": "写作、头脑风暴、创意"
},
"noLogs": "(暂无日志)",
"toolbar": {
"refresh": "刷新聊天",
"showThinking": "显示思考过程",
"hideThinking": "隐藏思考过程",
"sessionModel": "会话模型",
"sessionModelDefault": "跟随 Agent 默认模型",
"sessionModelLoading": "正在加载模型列表...",
"sessionModelUpdateFailed": "切换会话模型失败"
},
"historyBuckets": {
"today": "今天",
"yesterday": "昨天",
"withinWeek": "一周内",
"withinTwoWeeks": "两周内",
"withinMonth": "一个月内",
"older": "一个月之前"
}
}
78 changes: 74 additions & 4 deletions src/pages/Chat/ChatToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,96 @@
/**
* Chat Toolbar
* Session selector, new session, refresh, and thinking toggle.
* Session model selector, refresh, and thinking toggle.
* Rendered in the Header when on the Chat page.
*/
import { RefreshCw, Brain } from 'lucide-react';
import { useEffect, type ChangeEvent } from 'react';
import { RefreshCw, Brain, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { toUserMessage } from '@/lib/api-client';
import { useChatStore } from '@/stores/chat';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';

const DEFAULT_MODEL_VALUE = '__agent_default__';

function buildModelValue(provider?: string, model?: string): string {
if (!provider || !model) return DEFAULT_MODEL_VALUE;
return `${provider}/${model}`;
}

function formatModelOptionLabel(name: string, provider: string): string {
return `${name} · ${provider}`;
}

export function ChatToolbar() {
const refresh = useChatStore((s) => s.refresh);
const loading = useChatStore((s) => s.loading);
const sending = useChatStore((s) => s.sending);
const showThinking = useChatStore((s) => s.showThinking);
const toggleThinking = useChatStore((s) => s.toggleThinking);
const currentSessionKey = useChatStore((s) => s.currentSessionKey);
const currentSession = useChatStore((s) => s.sessions.find((session) => session.key === currentSessionKey));
const sessionModelOptions = useChatStore((s) => s.sessionModelOptions);
const sessionModelLoading = useChatStore((s) => s.sessionModelLoading);
const sessionModelSaving = useChatStore((s) => s.sessionModelSaving);
const loadSessionModelOptions = useChatStore((s) => s.loadSessionModelOptions);
const updateCurrentSessionModel = useChatStore((s) => s.updateCurrentSessionModel);
const { t } = useTranslation('chat');

useEffect(() => {
void loadSessionModelOptions();
}, [loadSessionModelOptions]);

const currentModelValue = buildModelValue(currentSession?.modelProvider, currentSession?.model);
const hasCurrentModelOption = currentModelValue === DEFAULT_MODEL_VALUE
|| sessionModelOptions.some((option) => option.value === currentModelValue);
const currentModelFallbackLabel = currentSession?.model && currentSession?.modelProvider
? formatModelOptionLabel(currentSession.model, currentSession.modelProvider)
: null;
const disableModelSelect = sending || sessionModelSaving || (sessionModelLoading && sessionModelOptions.length === 0);

const handleModelChange = async (event: ChangeEvent<HTMLSelectElement>) => {
const nextValue = event.target.value;
try {
await updateCurrentSessionModel(nextValue === DEFAULT_MODEL_VALUE ? null : nextValue);
} catch (error) {
toast.error(`${t('toolbar.sessionModelUpdateFailed')}: ${toUserMessage(error)}`);
}
};

return (
<div className="flex items-center gap-2">
{/* Refresh */}
<Select
aria-label={t('toolbar.sessionModel')}
className="h-8 min-w-[220px] max-w-[320px] text-xs"
value={currentModelValue}
onChange={handleModelChange}
disabled={disableModelSelect}
title={currentModelValue === DEFAULT_MODEL_VALUE
? t('toolbar.sessionModelDefault')
: currentModelFallbackLabel || currentModelValue}
>
<option value={DEFAULT_MODEL_VALUE}>{t('toolbar.sessionModelDefault')}</option>
{!hasCurrentModelOption && currentModelFallbackLabel ? (
<option value={currentModelValue}>{currentModelFallbackLabel}</option>
) : null}
{sessionModelLoading && sessionModelOptions.length === 0 ? (
<option value={DEFAULT_MODEL_VALUE} disabled>{t('toolbar.sessionModelLoading')}</option>
) : null}
{sessionModelOptions.map((option) => (
<option key={option.value} value={option.value}>
{formatModelOptionLabel(option.name, option.provider)}
</option>
))}
</Select>

{(sessionModelLoading || sessionModelSaving) ? (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
) : null}

<Tooltip>
<TooltipTrigger asChild>
<Button
Expand All @@ -37,7 +108,6 @@ export function ChatToolbar() {
</TooltipContent>
</Tooltip>

{/* Thinking Toggle */}
<Tooltip>
<TooltipTrigger asChild>
<Button
Expand Down
Loading