feat(desktop): wire template welcome_message + quick_commands to chat UI
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- Add welcomeMessage/quickCommands fields to Clone interface
- Persist template welcome/quick data via updateClone after creation
- FirstConversationPrompt: prefer template-provided welcome message
  over dynamically generated one
- FirstConversationPrompt: render template quick_commands as chips
  instead of hardcoded QUICK_ACTIONS when available
- Tighten assign/unassign template endpoint permissions from model:read
  to relay:use (self-service operation for all authenticated users)
This commit is contained in:
iven
2026-04-03 15:20:15 +08:00
parent 1048901665
commit 0857a1f608
3 changed files with 81 additions and 35 deletions

View File

@@ -14,6 +14,7 @@ import {
Microscope,
Layers,
GraduationCap,
MessageSquare,
} from 'lucide-react';
import { cn } from '../lib/utils';
import {
@@ -63,13 +64,15 @@ export function FirstConversationPrompt({
ultra: '多代理协作,全能力调度',
};
const welcomeMessage = generateWelcomeMessage({
userName: clone.userName,
agentName: clone.nickname || clone.name,
emoji: clone.emoji,
personality: clone.personality,
scenarios: clone.scenarios,
});
// Use template-provided welcome message if available, otherwise generate dynamically
const welcomeMessage = clone.welcomeMessage
|| generateWelcomeMessage({
userName: clone.userName,
agentName: clone.nickname || clone.name,
emoji: clone.emoji,
personality: clone.personality,
scenarios: clone.scenarios,
});
const handleQuickAction = (key: string) => {
if (key === 'learn') {
@@ -149,32 +152,54 @@ export function FirstConversationPrompt({
</p>
</div>
{/* Quick action chips — DeerFlow-style horizontal colored pills */}
{/* Quick action chips — template-provided or DeerFlow-style defaults */}
<div className="flex items-center justify-center gap-2 flex-wrap">
{QUICK_ACTIONS.map((action, index) => {
const ActionIcon = action.icon;
return (
<motion.button
key={action.key}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.05, duration: 0.2 }}
onClick={() => handleQuickAction(action.key)}
className={cn(
'flex items-center gap-2 px-4 py-2',
'bg-white dark:bg-gray-800',
'border border-gray-200 dark:border-gray-700',
'rounded-full text-sm text-gray-600 dark:text-gray-300',
'hover:border-gray-300 dark:hover:border-gray-600',
'hover:bg-gray-50 dark:hover:bg-gray-750',
'transition-all duration-150'
)}
>
<ActionIcon className={`w-4 h-4 ${action.color}`} />
<span>{action.label}</span>
</motion.button>
);
})}
{clone.quickCommands && clone.quickCommands.length > 0
? clone.quickCommands.map((cmd, index) => (
<motion.button
key={cmd.label}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.05, duration: 0.2 }}
onClick={() => onSelectSuggestion?.(cmd.command)}
className={cn(
'flex items-center gap-2 px-4 py-2',
'bg-white dark:bg-gray-800',
'border border-gray-200 dark:border-gray-700',
'rounded-full text-sm text-gray-600 dark:text-gray-300',
'hover:border-gray-300 dark:hover:border-gray-600',
'hover:bg-gray-50 dark:hover:bg-gray-750',
'transition-all duration-150'
)}
>
<MessageSquare className="w-4 h-4 text-primary" />
<span>{cmd.label}</span>
</motion.button>
))
: QUICK_ACTIONS.map((action, index) => {
const ActionIcon = action.icon;
return (
<motion.button
key={action.key}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.05, duration: 0.2 }}
onClick={() => handleQuickAction(action.key)}
className={cn(
'flex items-center gap-2 px-4 py-2',
'bg-white dark:bg-gray-800',
'border border-gray-200 dark:border-gray-700',
'rounded-full text-sm text-gray-600 dark:text-gray-300',
'hover:border-gray-300 dark:hover:border-gray-600',
'hover:bg-gray-50 dark:hover:bg-gray-750',
'transition-all duration-150'
)}
>
<ActionIcon className={`w-4 h-4 ${action.color}`} />
<span>{action.label}</span>
</motion.button>
);
})}
</div>
{/* Scenario tags */}

View File

@@ -36,6 +36,8 @@ export interface Clone {
notes?: string; // 用户备注
onboardingCompleted?: boolean; // 是否完成首次引导
source_template_id?: string; // 模板来源 ID
welcomeMessage?: string; // 模板预设欢迎语
quickCommands?: Array<{ label: string; command: string }>; // 模板预设快捷命令
}
export interface UsageStats {
@@ -226,10 +228,29 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
console.warn('Failed to persist temperature/max_tokens:', e);
}
}
// Persist welcome_message + quick_commands as clone metadata
if (template.welcome_message || (template.quick_commands && template.quick_commands.length > 0)) {
try {
await client.updateClone(cloneId, {
welcomeMessage: template.welcome_message || '',
quickCommands: template.quick_commands || [],
});
} catch (e) {
console.warn('Failed to persist welcome_message/quick_commands:', e);
}
}
}
await get().loadClones();
return result?.clone;
// Merge template welcome/quick data into the returned clone for immediate use
const createdClone = result?.clone as Record<string, unknown> | undefined;
if (createdClone) {
if (template.welcome_message) createdClone.welcomeMessage = template.welcome_message;
if (template.quick_commands?.length) createdClone.quickCommands = template.quick_commands;
}
return createdClone as Clone | undefined;
} catch (error) {
set({ error: String(error) });
return undefined;