From 35faf3717d05466c447a514810e3504385b1eee7 Mon Sep 17 00:00:00 2001 From: GCWing Date: Thu, 19 Mar 2026 19:50:18 +0800 Subject: [PATCH] refactor: remove prompt template feature - Remove PromptTemplateConfig component and related files - Remove TemplatePickerPanel and template editing functionality - Remove PromptTemplateService and PromptTemplateAPI - Remove prompt template types and parser utilities - Remove prompt template related API endpoints in Rust backend - Remove prompt template i18n translation files - Clean up references in ChatInput, PersonaView, and settings This simplifies the codebase by removing unused prompt template functionality. --- src/apps/desktop/src/api/mod.rs | 1 - .../desktop/src/api/prompt_template_api.rs | 127 ------ src/apps/desktop/src/lib.rs | 5 - src/crates/core/src/service/config/types.rs | 2 - .../app/scenes/profile/views/PersonaView.tsx | 43 +- .../src/app/scenes/settings/SettingsScene.tsx | 3 - .../src/app/scenes/settings/settingsConfig.ts | 2 - .../src/flow_chat/components/ChatInput.scss | 167 ------- .../src/flow_chat/components/ChatInput.tsx | 135 +----- .../components/TemplatePickerPanel.scss | 409 ----------------- .../components/TemplatePickerPanel.tsx | 398 ----------------- .../src/flow_chat/hooks/useTemplateEditor.ts | 284 ------------ .../src/flow_chat/reducers/templateReducer.ts | 87 ---- .../api/service-api/PromptTemplateAPI.ts | 56 --- .../config/components/EditorConfig.tsx | 4 +- .../components/PromptTemplateConfig.scss | 37 -- .../components/PromptTemplateConfig.tsx | 378 ---------------- .../infrastructure/i18n/core/I18nService.ts | 5 - .../src/infrastructure/i18n/types/index.ts | 1 - .../services/PromptTemplateService.ts | 414 ------------------ .../services/ShortcutManager.ts | 2 +- src/web-ui/src/locales/en-US/flow-chat.json | 3 - .../src/locales/en-US/scenes/profile.json | 5 +- src/web-ui/src/locales/en-US/settings.json | 12 - .../src/locales/en-US/settings/editor.json | 3 +- .../en-US/settings/prompt-templates.json | 92 ---- src/web-ui/src/locales/zh-CN/flow-chat.json | 3 - .../src/locales/zh-CN/scenes/profile.json | 5 +- src/web-ui/src/locales/zh-CN/settings.json | 12 - .../src/locales/zh-CN/settings/editor.json | 3 +- .../zh-CN/settings/prompt-templates.json | 92 ---- .../src/shared/types/prompt-template.ts | 86 ---- src/web-ui/src/shared/types/shortcut.ts | 11 + src/web-ui/src/shared/utils/templateParser.ts | 124 ------ 34 files changed, 27 insertions(+), 2984 deletions(-) delete mode 100644 src/apps/desktop/src/api/prompt_template_api.rs delete mode 100644 src/web-ui/src/flow_chat/components/TemplatePickerPanel.scss delete mode 100644 src/web-ui/src/flow_chat/components/TemplatePickerPanel.tsx delete mode 100644 src/web-ui/src/flow_chat/hooks/useTemplateEditor.ts delete mode 100644 src/web-ui/src/flow_chat/reducers/templateReducer.ts delete mode 100644 src/web-ui/src/infrastructure/api/service-api/PromptTemplateAPI.ts delete mode 100644 src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.scss delete mode 100644 src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx delete mode 100644 src/web-ui/src/infrastructure/services/PromptTemplateService.ts delete mode 100644 src/web-ui/src/locales/en-US/settings/prompt-templates.json delete mode 100644 src/web-ui/src/locales/zh-CN/settings/prompt-templates.json delete mode 100644 src/web-ui/src/shared/types/prompt-template.ts create mode 100644 src/web-ui/src/shared/types/shortcut.ts delete mode 100644 src/web-ui/src/shared/utils/templateParser.ts diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index 1dedde63..cf3245a2 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -21,7 +21,6 @@ pub mod lsp_workspace_api; pub mod mcp_api; pub mod miniapp_api; pub mod project_context_api; -pub mod prompt_template_api; pub mod remote_connect_api; pub mod runtime_api; pub mod session_api; diff --git a/src/apps/desktop/src/api/prompt_template_api.rs b/src/apps/desktop/src/api/prompt_template_api.rs deleted file mode 100644 index 31b346fe..00000000 --- a/src/apps/desktop/src/api/prompt_template_api.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Prompt Template Management API - -use crate::api::app_state::AppState; -use log::{error, warn}; -use serde::{Deserialize, Serialize}; -use tauri::State; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PromptTemplate { - pub id: String, - pub name: String, - pub description: Option, - pub content: String, - pub category: Option, - pub shortcut: Option, - pub is_favorite: bool, - pub order: i32, - pub created_at: i64, - pub updated_at: i64, - pub usage_count: i32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PromptTemplateConfig { - pub templates: Vec, - pub global_shortcut: String, - pub enable_auto_complete: bool, - pub recent_templates: Vec, - pub last_sync_time: Option, -} - -impl Default for PromptTemplateConfig { - fn default() -> Self { - Self { - templates: Vec::new(), - global_shortcut: "Ctrl+Shift+P".to_string(), - enable_auto_complete: true, - recent_templates: Vec::new(), - last_sync_time: None, - } - } -} - -#[tauri::command] -pub async fn get_prompt_template_config( - state: State<'_, AppState>, -) -> Result { - let config_service = &state.config_service; - - match config_service - .get_config::>(Some("prompt_templates")) - .await - { - Ok(Some(config)) => Ok(config), - Ok(None) => { - let default_config = create_default_config(); - if let Err(e) = config_service - .set_config("prompt_templates", &default_config) - .await - { - warn!("Failed to save default config: error={}", e); - } - Ok(default_config) - } - Err(e) => { - error!("Failed to get prompt template config: error={}", e); - Ok(create_default_config()) - } - } -} - -#[tauri::command] -pub async fn save_prompt_template_config( - state: State<'_, AppState>, - config: PromptTemplateConfig, -) -> Result<(), String> { - let config_service = &state.config_service; - - config_service - .set_config("prompt_templates", config) - .await - .map_err(|e| { - error!("Failed to save prompt template config: error={}", e); - format!("Failed to save config: {}", e) - }) -} - -#[tauri::command] -pub async fn export_prompt_templates(state: State<'_, AppState>) -> Result { - let config = get_prompt_template_config(state).await?; - - serde_json::to_string_pretty(&config).map_err(|e| { - error!("Failed to export prompt templates: error={}", e); - format!("Export failed: {}", e) - }) -} - -#[tauri::command] -pub async fn import_prompt_templates( - state: State<'_, AppState>, - json: String, -) -> Result<(), String> { - let config: PromptTemplateConfig = - serde_json::from_str(&json).map_err(|e| format!("Invalid config format: {}", e))?; - - save_prompt_template_config(state, config).await -} - -#[tauri::command] -pub async fn reset_prompt_templates(state: State<'_, AppState>) -> Result<(), String> { - let default_config = create_default_config(); - save_prompt_template_config(state, default_config).await -} - -fn create_default_config() -> PromptTemplateConfig { - let now = chrono::Utc::now().timestamp_millis(); - - PromptTemplateConfig { - templates: Vec::new(), - global_shortcut: "Ctrl+Shift+P".to_string(), - enable_auto_complete: true, - recent_templates: Vec::new(), - last_sync_time: Some(now), - } -} diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index e9bfc043..d42277d6 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -561,11 +561,6 @@ pub async fn run() { create_cron_job, update_cron_job, delete_cron_job, - api::prompt_template_api::get_prompt_template_config, - api::prompt_template_api::save_prompt_template_config, - api::prompt_template_api::export_prompt_templates, - api::prompt_template_api::import_prompt_templates, - api::prompt_template_api::reset_prompt_templates, api::config_api::sync_tool_configs, api::terminal_api::terminal_get_shells, api::terminal_api::terminal_create, diff --git a/src/crates/core/src/service/config/types.rs b/src/crates/core/src/service/config/types.rs index 430cfb84..3a6c51da 100644 --- a/src/crates/core/src/service/config/types.rs +++ b/src/crates/core/src/service/config/types.rs @@ -17,7 +17,6 @@ pub struct GlobalConfig { pub terminal: TerminalConfig, pub workspace: WorkspaceConfig, pub ai: AIConfig, - pub prompt_templates: Option, /// MCP server configuration (stored uniformly; supports both JSON and structured formats). #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option, @@ -897,7 +896,6 @@ impl Default for GlobalConfig { terminal: TerminalConfig::default(), workspace: WorkspaceConfig::default(), ai: AIConfig::default(), - prompt_templates: None, mcp_servers: None, themes: Some(ThemesConfig::default()), version: "1.0.0".to_string(), diff --git a/src/web-ui/src/app/scenes/profile/views/PersonaView.tsx b/src/web-ui/src/app/scenes/profile/views/PersonaView.tsx index 71717aa6..8ebb8606 100644 --- a/src/web-ui/src/app/scenes/profile/views/PersonaView.tsx +++ b/src/web-ui/src/app/scenes/profile/views/PersonaView.tsx @@ -19,8 +19,6 @@ import { } from '@/component-library'; import { AIRulesAPI, RuleLevel, type AIRule } from '@/infrastructure/api/service-api/AIRulesAPI'; import { getAllMemories, toggleMemory, type AIMemory } from '@/infrastructure/api/aiMemoryApi'; -import { promptTemplateService } from '@/infrastructure/services/PromptTemplateService'; -import type { PromptTemplate } from '@/shared/types/prompt-template'; import { MCPAPI, type MCPServerInfo } from '@/infrastructure/api/service-api/MCPAPI'; import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; import { configManager } from '@/infrastructure/config/services/ConfigManager'; @@ -323,7 +321,6 @@ const PersonaView: React.FC<{ workspacePath: string }> = ({ workspacePath }) => const [agenticConfig, setAgenticConfig] = useState(null); const [mcpServers, setMcpServers] = useState([]); const [skills, setSkills] = useState([]); - const [templates, setTemplates] = useState([]); const [aiExp, setAiExp] = useState>({ enable_visual_mode: false, enable_session_title_generation: true, @@ -358,7 +355,6 @@ const PersonaView: React.FC<{ workspacePath: string }> = ({ workspacePath }) => const memoryRef = useRef(null); const toolsRef = useRef(null); const skillsRef = useRef(null); - const templatesRef = useRef(null); const prefsRef = useRef(null); // detail section ref (kept for internal scroll-to section) @@ -388,16 +384,6 @@ const PersonaView: React.FC<{ workspacePath: string }> = ({ workspacePath }) => })(); }, [workspacePath]); - useEffect(() => { - const init = async () => { - try { await promptTemplateService.initialize(); } finally { - setTemplates(promptTemplateService.getAllTemplates()); - } - }; - init(); - return promptTemplateService.subscribe(() => setTemplates(promptTemplateService.getAllTemplates())); - }, []); - const loadCaps = useCallback(async () => { try { const { invoke } = await import('@tauri-apps/api/core'); @@ -539,7 +525,7 @@ const PersonaView: React.FC<{ workspacePath: string }> = ({ workspacePath }) => [t('radar.dims.memory')]: memoryRef, [t('radar.dims.autonomy')]: toolsRef, [t('radar.dims.adaptability')]: skillsRef, - [t('radar.dims.creativity')]: templatesRef, + [t('radar.dims.creativity')]: prefsRef, [t('radar.dims.expression')]: prefsRef, }; if (zone) setActiveZone(zone); @@ -868,9 +854,6 @@ const PersonaView: React.FC<{ workspacePath: string }> = ({ workspacePath }) => const sortSkills = useMemo(() => [...skills].sort((a, b) => a.enabled !== b.enabled ? (a.enabled ? -1 : 1) : a.name.localeCompare(b.name)), [skills]); - const sortTemplates = useMemo(() => - [...templates].sort((a, b) => a.isFavorite !== b.isFavorite ? (a.isFavorite ? -1 : 1) : b.usageCount - a.usageCount), - [templates]); const userRulesList = useMemo( () => sortRules.filter(rule => rule.level === RuleLevel.User), [sortRules], @@ -908,17 +891,16 @@ const PersonaView: React.FC<{ workspacePath: string }> = ({ workspacePath }) => const memEn = useMemo(() => memories.filter(m => m.enabled).length, [memories]); const rulesEn = useMemo(() => rules.filter(r => r.enabled), [rules]); const avgImp = useMemo(() => memEn > 0 ? memories.filter(m => m.enabled).reduce((s, m) => s + m.importance, 0) / memEn : 0, [memories, memEn]); - const favCount = useMemo(() => templates.filter(t => t.isFavorite).length, [templates]); const radarDims = useMemo(() => [ - { label: t('radar.dims.creativity'), value: Math.min(10, templates.length * 0.6 + skillEn.length * 0.5) }, + { label: t('radar.dims.creativity'), value: Math.min(10, skillEn.length * 0.9 + mcpServers.length * 0.35) }, { label: t('radar.dims.rigor'), value: Math.min(10, rulesEn.length * 1.5) }, { label: t('radar.dims.autonomy'), value: agenticConfig?.enabled ? Math.min(10, 4 + (agenticConfig.available_tools?.length ?? 0) * 0.25 + mcpServers.length * 0.5) : Math.min(10, enabledTools * 0.3 + healthyMcp * 0.8) }, { label: t('radar.dims.memory'), value: Math.min(10, memEn * 0.7 + avgImp * 0.3) }, - { label: t('radar.dims.expression'), value: Math.min(10, templates.length * 0.5 + favCount * 1.2) }, + { label: t('radar.dims.expression'), value: Math.min(10, skillEn.length * 0.8 + enabledSkls * 0.4) }, { label: t('radar.dims.adaptability'), value: Math.min(10, skillEn.length * 1.2 + mcpServers.length * 0.8) }, - ], [templates, skillEn, rulesEn, agenticConfig, mcpServers, enabledTools, healthyMcp, memEn, avgImp, favCount, t]); + ], [skillEn, rulesEn, agenticConfig, mcpServers, enabledTools, healthyMcp, memEn, avgImp, enabledSkls, t]); // model slot current IDs (with fallbacks) const slotIds: Record = useMemo(() => ({ @@ -1372,23 +1354,6 @@ const PersonaView: React.FC<{ workspacePath: string }> = ({ workspacePath }) => {/* Interaction */}
-
-
- {t('cards.templates')} - {t('kpi.templateCount', { count: templates.length })} - -
-
- {sortTemplates.slice(0, 14).map(tmpl => ( - - {tmpl.isFavorite && '★ '}{tmpl.name} - - ))} - {templates.length === 0 && {t('empty.templates')}} -
-
{t('cards.preferences')} diff --git a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx index 539e14ee..9a6bf3e7 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx +++ b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx @@ -20,8 +20,6 @@ const LoggingConfig = lazy(() => import('../../../infrastructure/config/c const TerminalConfig = lazy(() => import('../../../infrastructure/config/components/TerminalConfig')); const EditorConfig = lazy(() => import('../../../infrastructure/config/components/EditorConfig')); const ThemeConfigComponent = lazy(() => import('../../../infrastructure/config/components/ThemeConfig').then(m => ({ default: m.ThemeConfig }))); -const PromptTemplateConfig = lazy(() => import('../../../infrastructure/config/components/PromptTemplateConfig')); - const SettingsScene: React.FC = () => { const activeTab = useSettingsStore(s => s.activeTab); @@ -32,7 +30,6 @@ const SettingsScene: React.FC = () => { case 'models': Content = AIModelConfig; break; case 'session-config': Content = SessionConfig; break; case 'ai-context': Content = AIRulesMemoryConfig; break; - case 'prompt-templates': Content = PromptTemplateConfig; break; case 'mcp-tools': Content = McpToolsConfig; break; case 'lsp': Content = LspConfig; break; case 'debug': Content = DebugConfig; break; diff --git a/src/web-ui/src/app/scenes/settings/settingsConfig.ts b/src/web-ui/src/app/scenes/settings/settingsConfig.ts index b215acc9..1ae5dd7c 100644 --- a/src/web-ui/src/app/scenes/settings/settingsConfig.ts +++ b/src/web-ui/src/app/scenes/settings/settingsConfig.ts @@ -10,7 +10,6 @@ export type ConfigTab = | 'models' | 'session-config' | 'ai-context' - | 'prompt-templates' | 'mcp-tools' | 'lsp' | 'debug' @@ -43,7 +42,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ nameKey: 'configCenter.categories.smartCapabilities', tabs: [ { id: 'session-config', labelKey: 'configCenter.tabs.sessionConfig' }, - { id: 'prompt-templates', labelKey: 'configCenter.tabs.promptTemplates' }, { id: 'ai-context', labelKey: 'configCenter.tabs.aiContext' }, { id: 'mcp-tools', labelKey: 'configCenter.tabs.mcpTools' }, ], diff --git a/src/web-ui/src/flow_chat/components/ChatInput.scss b/src/web-ui/src/flow_chat/components/ChatInput.scss index ffdf5aee..d4d645f2 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.scss +++ b/src/web-ui/src/flow_chat/components/ChatInput.scss @@ -120,7 +120,6 @@ } .bitfun-chat-input__expand-button, - .bitfun-chat-input__template-hint, .bitfun-chat-input__cowork-examples, .bitfun-chat-input__recommendations, .bitfun-chat-input__target-switcher, @@ -282,31 +281,6 @@ } } - &--template-mode { - .bitfun-chat-input__box { - border-color: rgba(160, 140, 255, 0.25); - - &:focus-within { - border-color: rgba(160, 140, 255, 0.36); - box-shadow: - 0 12px 32px rgba(0, 0, 0, 0.4), - 0 0 20px rgba(160, 140, 255, 0.15), - 0 0 40px rgba(140, 100, 255, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.12); - - &::before { - opacity: 0.38; - background: linear-gradient( - 135deg, - rgba(160, 120, 255, 0.5) 0%, - rgba(140, 100, 255, 0.45) 50%, - rgba(180, 140, 255, 0.5) 100% - ); - } - } - } - } - // Non-agent scenes: high transparency to avoid obscuring content &--non-agent { .bitfun-chat-input__box { @@ -1273,147 +1247,6 @@ } } -.bitfun-chat-input__template-hint { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 8px 12px; - margin: 0 0 10px 0; - background: var(--element-bg-soft); - border: 1px solid var(--border-subtle); - border-radius: 8px; - font-size: 11px; - color: var(--color-text-secondary); - animation: slideInFromTop 0.25s ease-out; - min-width: 0; - backdrop-filter: blur(8px) saturate(1.1); - -webkit-backdrop-filter: blur(8px) saturate(1.1); - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.04); - transition: all 0.2s ease; - - &:hover { - background: var(--element-bg-base); - border-color: rgba(140, 120, 255, 0.15); - box-shadow: - 0 4px 12px rgba(0, 0, 0, 0.25), - 0 0 0 1px rgba(140, 120, 255, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.06); - } -} - -.bitfun-chat-input__template-hint-icon { - font-size: 14px; - flex-shrink: 0; - color: var(--color-text-muted); -} - -.bitfun-chat-input__template-hint-text { - flex: 1; - line-height: 1.5; - color: var(--color-text-secondary); - min-width: 0; - word-wrap: break-word; - - kbd { - display: inline-block; - padding: 2px 6px; - margin: 0 2px; - background: var(--element-bg-medium); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 4px; - font-size: 10px; - font-family: 'Consolas', 'Monaco', monospace; - color: var(--color-text-primary); - font-weight: 600; - white-space: nowrap; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - } -} - -.bitfun-chat-input__template-hint-progress { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - min-width: 24px; - height: 20px; - padding: 0 10px; - background: linear-gradient(135deg, - rgba(139, 92, 246, 0.15) 0%, - rgba(124, 58, 237, 0.12) 100%); - border: 1px solid rgba(139, 92, 246, 0.25); - color: var(--color-purple-400); - border-radius: 10px; - font-size: 10px; - font-weight: 600; - white-space: nowrap; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - box-shadow: - 0 2px 6px rgba(139, 92, 246, 0.15), - inset 0 1px 0 rgba(255, 255, 255, 0.08); - transition: all 0.2s ease; - - &::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: 11px; - padding: 1px; - background: linear-gradient( - 135deg, - rgba(139, 92, 246, 0) 0%, - rgba(139, 92, 246, 0) 100% - ); - -webkit-mask: - linear-gradient(#fff 0 0) content-box, - linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; - opacity: 0; - transition: opacity 0.2s ease; - pointer-events: none; - } - - &:hover { - background: linear-gradient(135deg, - rgba(139, 92, 246, 0.2) 0%, - rgba(124, 58, 237, 0.18) 100%); - border-color: rgba(139, 92, 246, 0.35); - color: var(--color-purple-300); - box-shadow: - 0 3px 8px rgba(139, 92, 246, 0.25), - 0 0 0 1px rgba(139, 92, 246, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.1); - transform: translateY(-1px); - - &::before { - opacity: 1; - background: linear-gradient( - 135deg, - rgba(139, 92, 246, 0.3) 0%, - rgba(124, 58, 237, 0.25) 50%, - rgba(139, 92, 246, 0.3) 100% - ); - } - } -} - -@keyframes slideInFromTop { - from { - opacity: 0; - transform: translateY(-12px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - @keyframes bitfun-chat-input-fade-in { from { opacity: 0; diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 9b612f80..d4422da8 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -5,7 +5,7 @@ import React, { useRef, useCallback, useEffect, useReducer, useState, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { ArrowUp, Image, Network, ChevronsUp, ChevronsDown, RotateCcw, FileText } from 'lucide-react'; +import { ArrowUp, Image, Network, ChevronsUp, ChevronsDown, RotateCcw } from 'lucide-react'; import { ContextDropZone, useContextStore } from '../../shared/context-system'; import { useActiveSessionState } from '../hooks/useActiveSessionState'; import { RichTextInput, type MentionState } from './RichTextInput'; @@ -18,22 +18,16 @@ import { ModelSelector } from './ModelSelector'; import { FlowChatStore } from '../store/FlowChatStore'; import type { FlowChatState } from '../types/flow-chat'; import type { FileContext, DirectoryContext } from '../../shared/types/context'; -import type { PromptTemplate } from '../../shared/types/prompt-template'; import { SmartRecommendations } from './smart-recommendations'; import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { WorkspaceKind } from '@/shared/types'; import { createImageContextFromFile, createImageContextFromClipboard } from '../utils/imageUtils'; import { notificationService } from '@/shared/notification-system'; -import { TemplatePickerPanel } from './TemplatePickerPanel'; -import { promptTemplateService } from '@/infrastructure/services/PromptTemplateService'; -import { shortcutManager } from '@/infrastructure/services/ShortcutManager'; import { inputReducer, initialInputState } from '../reducers/inputReducer'; -import { templateReducer, initialTemplateState } from '../reducers/templateReducer'; import { modeReducer, initialModeState } from '../reducers/modeReducer'; import { CHAT_INPUT_CONFIG } from '../constants/chatInputConfig'; import { MERMAID_INTERACTIVE_EXAMPLE } from '../constants/mermaidExamples'; import { useMessageSender } from '../hooks/useMessageSender'; -import { useTemplateEditor } from '../hooks/useTemplateEditor'; import { useChatInputState } from '../store/chatInputStateStore'; import { useInputHistoryStore } from '../store/inputHistoryStore'; import { startBtwThread } from '../services/BtwThreadService'; @@ -75,7 +69,6 @@ export const ChatInput: React.FC = ({ const { t } = useTranslation('flow-chat'); const [inputState, dispatchInput] = useReducer(inputReducer, initialInputState); - const [templateState, dispatchTemplate] = useReducer(templateReducer, initialTemplateState); const [modeState, dispatchMode] = useReducer(modeReducer, initialModeState); const richTextInputRef = useRef(null); @@ -177,40 +170,9 @@ export const ChatInput: React.FC = ({ contexts, onClearContexts: clearContexts, onSuccess: onSendMessage, - onExitTemplateMode: () => { - if (templateState.fillState?.isActive) { - dispatchTemplate({ type: 'EXIT_FILL' }); - - if (richTextInputRef.current) { - const editor = richTextInputRef.current as HTMLElement; - editor.innerHTML = ''; - } - } - }, currentAgentType: effectiveTargetSession?.mode || modeState.current, }); - const { - handleTemplateSelect: originalHandleTemplateSelect, - exitTemplateMode, - moveToNextPlaceholder, - moveToPrevPlaceholder, - } = useTemplateEditor({ - editorRef: richTextInputRef, - templateFillState: templateState.fillState, - onValueChange: (value: string) => dispatchInput({ type: 'SET_VALUE', payload: value }), - onStartFill: (state) => dispatchTemplate({ type: 'START_FILL', payload: state }), - onExitFill: () => dispatchTemplate({ type: 'EXIT_FILL' }), - onUpdateCurrentIndex: (index) => dispatchTemplate({ type: 'UPDATE_CURRENT_INDEX', payload: index }), - onNextPlaceholder: () => dispatchTemplate({ type: 'NEXT_PLACEHOLDER' }), - onPrevPlaceholder: () => dispatchTemplate({ type: 'PREV_PLACEHOLDER' }), - }); - - const handleTemplateSelect = useCallback((template: PromptTemplate) => { - dispatchInput({ type: 'ACTIVATE' }); - originalHandleTemplateSelect(template); - }, [originalHandleTemplateSelect]); - const [recommendationContext, setRecommendationContext] = React.useState<{ workspacePath?: string; sessionId?: string; @@ -265,39 +227,6 @@ export const ChatInput: React.FC = ({ return () => unsubscribe(); }, [effectiveTargetSessionId]); - React.useEffect(() => { - const initializeTemplateService = async () => { - await promptTemplateService.initialize(); - - const config = promptTemplateService.getConfig(); - const shortcutConfig = shortcutManager.parseShortcut(config.globalShortcut); - - if (shortcutConfig) { - const unregister = shortcutManager.register( - 'prompt-template-picker', - shortcutConfig, - () => { - dispatchTemplate({ type: 'OPEN_PICKER' }); - }, - { - description: 'Open prompt template picker panel', - priority: 10 - } - ); - - return unregister; - } - }; - - const unregisterPromise = initializeTemplateService(); - - return () => { - unregisterPromise.then(unregister => { - if (unregister) unregister(); - }); - }; - }, []); - React.useEffect(() => { const handleFillInput = (event: Event) => { const customEvent = event as CustomEvent<{ message: string }>; @@ -1036,27 +965,8 @@ export const ChatInput: React.FC = ({ } } - if (templateState.fillState?.isActive) { - if (e.key === 'Tab') { - e.preventDefault(); - - if (e.shiftKey) { - moveToPrevPlaceholder(); - } else { - moveToNextPlaceholder(); - } - return; - } - - if (e.key === 'Escape') { - e.preventDefault(); - exitTemplateMode(); - return; - } - } - // Tab key: toggle send target when the btw session switcher is visible - if (showTargetSwitcher && e.key === 'Tab' && !e.shiftKey && !slashCommandState.isActive && !templateState.fillState?.isActive) { + if (showTargetSwitcher && e.key === 'Tab' && !e.shiftKey && !slashCommandState.isActive) { e.preventDefault(); setInputTarget(prev => prev === 'main' ? 'btw' : 'main'); return; @@ -1161,9 +1071,6 @@ export const ChatInput: React.FC = ({ return; } - if (templateState.fillState?.isActive) { - exitTemplateMode(); - } handleSendOrCancel(); } @@ -1171,7 +1078,7 @@ export const ChatInput: React.FC = ({ e.preventDefault(); transition(SessionExecutionEvent.USER_CANCEL); } - }, [handleSendOrCancel, submitBtwFromInput, derivedState, transition, templateState.fillState, moveToNextPlaceholder, moveToPrevPlaceholder, exitTemplateMode, slashCommandState, getFilteredModes, getFilteredActions, getSlashPickerItems, selectSlashCommandMode, selectSlashCommandAction, canSwitchModes, historyIndex, inputHistory, savedDraft, inputState.value, currentSessionId, isBtwSession, showTargetSwitcher, setInputTarget, t]); + }, [handleSendOrCancel, submitBtwFromInput, derivedState, transition, slashCommandState, getFilteredModes, getFilteredActions, getSlashPickerItems, selectSlashCommandMode, selectSlashCommandAction, canSwitchModes, historyIndex, inputHistory, savedDraft, inputState.value, currentSessionId, isBtwSession, showTargetSwitcher, setInputTarget, t]); const handleImeCompositionStart = useCallback(() => { isImeComposingRef.current = true; @@ -1384,12 +1291,6 @@ export const ChatInput: React.FC = ({ return ( <> - dispatchTemplate({ type: 'CLOSE_PICKER' })} - onSelect={handleTemplateSelect} - /> - = ({ >
@@ -1420,15 +1321,6 @@ export const ChatInput: React.FC = ({ )}
- {templateState.fillState?.isActive && ( -
- - - {t('chatInput.templateProgress', { current: templateState.fillState.currentIndex + 1, total: templateState.fillState.placeholders.length })} - -
- )} -
{showTargetSwitcher && (
@@ -1613,15 +1505,6 @@ export const ChatInput: React.FC = ({ variant="ghost" size="xs" onClick={() => { - if (templateState.fillState?.isActive) { - dispatchTemplate({ type: 'EXIT_FILL' }); - - if (richTextInputRef.current) { - const editor = richTextInputRef.current as HTMLElement; - editor.innerHTML = ''; - } - } - dispatchInput({ type: 'CLEAR_VALUE' }); setQueuedInput(null); }} @@ -1740,16 +1623,6 @@ export const ChatInput: React.FC = ({ - dispatchTemplate({ type: 'TOGGLE_PICKER' })} - tooltip={t('input.selectPromptTemplate')} - > - - - {renderActionButton()}
diff --git a/src/web-ui/src/flow_chat/components/TemplatePickerPanel.scss b/src/web-ui/src/flow_chat/components/TemplatePickerPanel.scss deleted file mode 100644 index 63a6f17c..00000000 --- a/src/web-ui/src/flow_chat/components/TemplatePickerPanel.scss +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Template picker panel styles. - * Reference: panel style refinement. - */ - -@use '../../component-library/styles/tokens' as *; -@use '../../component-library/styles/_extended-mixins' as mixins; - -// ==================== Overlay ==================== - -.template-picker-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: $z-modal; - background: transparent; - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 12vh; -} - -// ==================== Panel container ==================== - -.template-picker-panel { - position: relative; - display: flex; - flex-direction: column; - background: var(--color-bg-primary); - border: 1px solid var(--border-subtle); - border-radius: $size-radius-base; - width: 90%; - max-width: 580px; - max-height: min(75vh, 640px); - overflow: hidden; - box-shadow: - 0 24px 48px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(255, 255, 255, 0.03), - 0 0 80px rgba(139, 92, 246, 0.06); - animation: template-panel-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1); -} - -@keyframes template-panel-enter { - from { - opacity: 0; - transform: translateY(20px) scale(0.97); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -// ==================== Header ==================== - -.template-picker-panel__header { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 12px; - padding: 16px 16px 14px; - position: relative; - - // Bottom gradient divider - &::after { - content: ''; - position: absolute; - bottom: 0; - left: 16px; - right: 16px; - height: 1px; - background: linear-gradient(90deg, transparent 0%, var(--border-subtle) 20%, var(--border-subtle) 80%, transparent 100%); - } -} - -// Search container -.template-picker-panel__search { - flex: 1; - min-width: 200px; -} - -.template-picker-panel__close-button { - flex-shrink: 0; - background: transparent; - border: 1px solid transparent; - color: var(--color-text-secondary); - padding: 6px; - border-radius: 6px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - - &:hover { - background: var(--element-bg-subtle); - border-color: var(--border-subtle); - color: var(--color-text-primary); - } - - &:active { - transform: scale(0.92); - } -} - -// ==================== Content area ==================== - -.template-picker-panel__content { - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: hidden; - padding: 12px; -} - -.template-picker-panel__section { - margin-bottom: 12px; - animation: template-section-enter 0.3s ease-out; - - &:last-child { - margin-bottom: 0; - } -} - -@keyframes template-section-enter { - from { - opacity: 0; - transform: translateY(6px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.template-picker-panel__section-title { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 10px 10px; - font-size: 11px; - font-weight: 500; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.4px; -} - -.template-picker-panel__count { - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: auto; - min-width: 20px; - height: 18px; - padding: 0 6px; - background: var(--color-accent-100); - color: var(--color-accent-600); - border-radius: 9px; - font-size: 10px; - font-weight: 600; -} - -// ==================== Empty state ==================== - -.template-picker-panel__empty { - padding: 48px 24px; - text-align: center; - animation: template-section-enter 0.3s ease-out; - - p { - margin: 0 0 8px 0; - - &:first-child { - font-size: 14px; - font-weight: 500; - color: var(--color-text-primary); - } - } -} - -.template-picker-panel__empty-hint { - font-size: 12px; - color: var(--color-text-secondary); -} - -// ==================== Template item ==================== - -.template-picker-panel__item { - position: relative; - display: flex; - flex-direction: column; - padding: 10px 12px; - cursor: pointer; - transition: all 0.2s ease; - background: transparent; - border: 1px solid transparent; - border-radius: 8px; - margin-bottom: 2px; - - &:last-child { - margin-bottom: 0; - } - - &:hover { - background: var(--element-bg-subtle); - border-color: var(--border-subtle); - } -} - -.template-picker-panel__item-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 4px; - min-height: 0; -} - -.template-picker-panel__item-title { - display: flex; - align-items: center; - gap: 6px; - font-size: 13px; - font-weight: 500; - color: var(--color-text-primary); - line-height: 1.4; - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - transition: color 0.2s ease; -} - -.template-picker-panel__item-star { - color: var(--color-warning); - flex-shrink: 0; -} - -.template-picker-panel__item-meta { - display: flex; - align-items: center; - gap: 6px; - flex-shrink: 0; -} - -.template-picker-panel__item-category { - font-size: 10px; - padding: 2px 6px; - background: var(--color-accent-100); - color: var(--color-accent-600); - border-radius: 4px; - font-weight: 500; - line-height: 1; - white-space: nowrap; -} - -.template-picker-panel__item-shortcut { - font-size: 9px; - padding: 2px 5px; - background: transparent; - color: var(--color-text-secondary); - border: 1px solid var(--border-base); - border-radius: 4px; - font-family: 'Consolas', 'Monaco', monospace; - font-weight: 600; - line-height: 1; - white-space: nowrap; -} - -.template-picker-panel__item-description { - font-size: 11px; - color: var(--color-text-secondary); - margin-bottom: 6px; - line-height: 1.5; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.template-picker-panel__item-preview { - font-size: 11px; - color: var(--color-text-secondary); - line-height: 1.5; - font-family: 'Consolas', 'Monaco', monospace; - background: var(--element-bg-subtle); - padding: 8px 10px; - border-radius: 6px; - border: 1px dashed var(--border-base); - white-space: pre-wrap; - word-break: break-word; - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; -} - -.template-picker-panel__item-placeholder { - color: var(--color-accent-600); - background: var(--color-accent-100); - padding: 1px 4px; - border-radius: 3px; - font-weight: 500; -} - -.template-picker-panel__item-matches { - font-size: 10px; - color: var(--color-accent-600); - margin-top: 6px; - font-weight: 500; -} - -.template-picker-panel__item-usage { - font-size: 10px; - color: var(--color-text-secondary); - margin-top: 4px; -} - -// ==================== Footer ==================== - -.template-picker-panel__footer { - flex-shrink: 0; - padding: 12px 16px; - position: relative; - - // Top gradient divider - &::before { - content: ''; - position: absolute; - top: 0; - left: 16px; - right: 16px; - height: 1px; - background: linear-gradient(90deg, transparent 0%, var(--border-subtle) 20%, var(--border-subtle) 80%, transparent 100%); - } -} - -.template-picker-panel__hints { - display: flex; - align-items: center; - justify-content: center; - gap: 16px; - flex-wrap: wrap; - font-size: 11px; - color: var(--color-text-secondary); - - span { - display: flex; - align-items: center; - gap: 6px; - } - - kbd { - font-size: 10px; - padding: 3px 6px; - background: transparent; - border: 1px solid var(--border-base); - border-radius: 4px; - font-family: 'Consolas', 'Monaco', monospace; - font-weight: 600; - color: var(--color-text-primary); - } -} - -// ==================== Responsive layout ==================== - -@media (max-width: 640px) { - .template-picker-overlay { - padding-top: 8vh; - padding-left: 12px; - padding-right: 12px; - } - - .template-picker-panel { - width: 100%; - max-height: min(70vh, 500px); - border-radius: 12px; - } - - .template-picker-panel__header { - padding: 12px; - - &::after { - left: 12px; - right: 12px; - } - } - - .template-picker-panel__content { - padding: 10px; - } - - .template-picker-panel__footer { - padding: 10px 12px; - - &::before { - left: 12px; - right: 12px; - } - } - - .template-picker-panel__hints { - gap: 12px; - } -} - diff --git a/src/web-ui/src/flow_chat/components/TemplatePickerPanel.tsx b/src/web-ui/src/flow_chat/components/TemplatePickerPanel.tsx deleted file mode 100644 index 27c8ee00..00000000 --- a/src/web-ui/src/flow_chat/components/TemplatePickerPanel.tsx +++ /dev/null @@ -1,398 +0,0 @@ -/** - * Template picker panel. - * Floating panel for quick prompt template selection. - */ - -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { Star, X } from 'lucide-react'; -import { Search } from '@/component-library'; -import { promptTemplateService } from '@/infrastructure/services/PromptTemplateService'; -import { PromptTemplate, TemplateSearchResult } from '@/shared/types/prompt-template'; -import { parseTemplate } from '@/shared/utils/templateParser'; -import { Tooltip } from '@/component-library'; -import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; -import './TemplatePickerPanel.scss'; - -export interface TemplatePickerPanelProps { - isOpen: boolean; - onClose: () => void; - onSelect: (template: PromptTemplate) => void; -} - -export const TemplatePickerPanel: React.FC = ({ - isOpen, - onClose, - onSelect -}) => { - const { t } = useI18n('settings/prompt-templates'); - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(0); - const [recentTemplates, setRecentTemplates] = useState([]); - const [favoriteTemplates, setFavoriteTemplates] = useState([]); - - const searchInputRef = useRef(null); - const panelRef = useRef(null); - const listRef = useRef(null); - - useEffect(() => { - if (isOpen) { - loadTemplates(); - setSearchQuery(''); - setSelectedIndex(0); - - // Focus the search input after mount. - setTimeout(() => { - searchInputRef.current?.focus?.(); - }, 100); - } - }, [isOpen]); - - const loadTemplates = useCallback(() => { - const recent = promptTemplateService.getRecentTemplates(5); - const favorite = promptTemplateService.getFavoriteTemplates(); - const all = promptTemplateService.getAllTemplates(); - - setRecentTemplates(recent); - setFavoriteTemplates(favorite); - - setSearchResults( - all.map(template => ({ - template, - matchScore: 1, - matchedFields: [] - })) - ); - }, []); - - useEffect(() => { - if (!isOpen) return; - - const results = promptTemplateService.searchTemplates(searchQuery); - setSearchResults(results); - setSelectedIndex(0); - }, [searchQuery, isOpen]); - - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - setSelectedIndex(prev => - Math.min(prev + 1, searchResults.length - 1) - ); - break; - - case 'ArrowUp': - e.preventDefault(); - setSelectedIndex(prev => Math.max(prev - 1, 0)); - break; - - case 'Enter': - e.preventDefault(); - if (searchResults[selectedIndex]) { - handleSelect(searchResults[selectedIndex].template); - } - break; - - case 'Escape': - e.preventDefault(); - onClose(); - break; - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, searchResults, selectedIndex, onClose]); - - useEffect(() => { - if (!listRef.current) return; - - const selectedElement = listRef.current.querySelector( - `[data-index="${selectedIndex}"]` - ) as HTMLElement; - - if (selectedElement) { - selectedElement.scrollIntoView({ - block: 'nearest', - behavior: 'smooth' - }); - } - }, [selectedIndex]); - - useEffect(() => { - if (!isOpen) return; - - const handleClickOutside = (e: MouseEvent) => { - if (panelRef.current && !panelRef.current.contains(e.target as Node)) { - onClose(); - } - }; - - setTimeout(() => { - document.addEventListener('mousedown', handleClickOutside); - }, 100); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen, onClose]); - - const handleSelect = useCallback((template: PromptTemplate) => { - promptTemplateService.recordUsage(template.id); - onSelect(template); - onClose(); - }, [onSelect, onClose]); - - if (!isOpen) { - return null; - } - - return ( -
-
-
-
- setSearchQuery(val)} - clearable - size="small" - autoFocus - /> -
- - - -
- -
- {searchQuery === '' && recentTemplates.length > 0 && ( -
-
- {t('picker.sectionRecent')} -
- {recentTemplates.map((template) => { - const index = searchResults.findIndex(r => r.template.id === template.id); - return ( - handleSelect(template)} - dataIndex={index} - /> - ); - })} -
- )} - - {searchQuery === '' && favoriteTemplates.length > 0 && ( -
-
- {t('picker.sectionFavorites')} -
- {favoriteTemplates.map((template) => { - const index = searchResults.findIndex(r => r.template.id === template.id); - return ( - handleSelect(template)} - dataIndex={index} - /> - ); - })} -
- )} - - {searchQuery !== '' && searchResults.length === 0 && ( -
-

{t('picker.emptyNoMatch')}

-

- {t('picker.emptyHint')} -

-
- )} - - {searchQuery !== '' && searchResults.length > 0 && ( -
-
- {t('picker.sectionSearchResults')} - - {searchResults.length} - -
- {searchResults.map((result, index) => ( - handleSelect(result.template)} - matchedFields={result.matchedFields} - dataIndex={index} - /> - ))} -
- )} - - {searchQuery === '' && searchResults.length > 0 && ( -
-
- {t('picker.sectionAllTemplates')} - - {searchResults.length} - -
- {searchResults.map((result, index) => ( - handleSelect(result.template)} - dataIndex={index} - /> - ))} -
- )} -
- -
-
- ↑↓ {t('picker.hintNavigate')} - Enter {t('picker.hintSelect')} - Esc {t('picker.hintClose')} -
-
-
-
- ); -}; - -// Template item component. -interface TemplateItemProps { - template: PromptTemplate; - isSelected: boolean; - onClick: () => void; - matchedFields?: string[]; - dataIndex: number; -} - -const TemplateItem: React.FC = ({ - template, - isSelected, - onClick, - matchedFields, - dataIndex -}) => { - const { t } = useI18n('settings/prompt-templates'); - // Render a preview with highlighted placeholders. - const renderTemplatePreview = (content: string) => { - const placeholders = parseTemplate(content); - - if (placeholders.length === 0) { - return content.length > 100 ? content.substring(0, 100) + '...' : content; - } - - // Truncate to 100 chars and only highlight placeholders in range. - const displayContent = content.length > 100 ? content.substring(0, 100) + '...' : content; - const elements: React.ReactNode[] = []; - let lastIndex = 0; - - placeholders.forEach((placeholder, index) => { - if (placeholder.startIndex >= 100) return; - - if (placeholder.startIndex > lastIndex) { - elements.push( - - {displayContent.substring(lastIndex, placeholder.startIndex)} - - ); - } - - const endIndex = Math.min(placeholder.endIndex, 100); - elements.push( - - {displayContent.substring(placeholder.startIndex, endIndex)} - - ); - - lastIndex = endIndex; - }); - - if (lastIndex < displayContent.length) { - elements.push( - - {displayContent.substring(lastIndex)} - - ); - } - - return <>{elements}; - }; - - return ( -
-
-
- {template.isFavorite && ( - - )} - {template.name} -
-
- {template.category && ( - - {template.category} - - )} - {template.shortcut && ( - - {template.shortcut} - - )} -
-
- - {template.description && ( -
- {template.description} -
- )} - -
- {renderTemplatePreview(template.content)} -
- - {matchedFields && matchedFields.length > 0 && ( -
- {t('picker.matches', { fields: matchedFields.join(', ') })} -
- )} - - {template.usageCount > 0 && ( -
- {t('picker.usageCount', { count: template.usageCount })} -
- )} -
- ); -}; - -export default TemplatePickerPanel; - diff --git a/src/web-ui/src/flow_chat/hooks/useTemplateEditor.ts b/src/web-ui/src/flow_chat/hooks/useTemplateEditor.ts deleted file mode 100644 index fec5c243..00000000 --- a/src/web-ui/src/flow_chat/hooks/useTemplateEditor.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * Template editor hook. - * Handles placeholder rendering, navigation, and fill state. - */ - -import { useCallback } from 'react'; -import type { PromptTemplate, PlaceholderInfo, PlaceholderFillState } from '@/shared/types/prompt-template'; -import { parseTemplate } from '@/shared/utils/templateParser'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('useTemplateEditor'); - -interface UseTemplateEditorProps { - /** Editor DOM ref */ - editorRef: React.RefObject; - /** Current template fill state */ - templateFillState: PlaceholderFillState | null; - /** Update input value */ - onValueChange: (value: string) => void; - /** Start template fill mode */ - onStartFill: (state: PlaceholderFillState) => void; - /** Exit fill mode */ - onExitFill: () => void; - /** Update current placeholder index */ - onUpdateCurrentIndex: (index: number) => void; - /** Move to next placeholder */ - onNextPlaceholder: () => void; - /** Move to previous placeholder */ - onPrevPlaceholder: () => void; -} - -interface UseTemplateEditorReturn { - /** Select a template */ - handleTemplateSelect: (template: PromptTemplate) => void; - /** Exit template mode */ - exitTemplateMode: () => void; - /** Move to next placeholder */ - moveToNextPlaceholder: () => boolean; - /** Move to previous placeholder */ - moveToPrevPlaceholder: () => boolean; -} - -export function useTemplateEditor(props: UseTemplateEditorProps): UseTemplateEditorReturn { - const { - editorRef, - templateFillState, - onValueChange, - onStartFill, - onExitFill, - onUpdateCurrentIndex, - onNextPlaceholder, - onPrevPlaceholder, - } = props; - - // Extract content from the editor, including placeholder values. - const extractTemplateContent = useCallback(() => { - if (!editorRef.current) return ''; - - const editor = editorRef.current as HTMLElement; - let result = ''; - - editor.childNodes.forEach(node => { - if (node.nodeType === Node.TEXT_NODE) { - result += node.textContent || ''; - } else if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as HTMLElement; - if (element.classList.contains('rich-text-placeholder')) { - result += element.textContent || ''; - } else { - result += element.textContent || ''; - } - } - }); - - return result; - }, [editorRef]); - - // Render content with highlighted placeholders. - const renderHighlightedTemplate = useCallback((content: string, placeholders: PlaceholderInfo[], currentIndex: number) => { - if (!editorRef.current) { - return; - } - - const editor = editorRef.current as HTMLElement; - - editor.innerHTML = ''; - - let lastIndex = 0; - const initialContentParts: string[] = []; - - placeholders.forEach((placeholder, index) => { - if (placeholder.startIndex > lastIndex) { - const textBefore = content.substring(lastIndex, placeholder.startIndex); - editor.appendChild(document.createTextNode(textBefore)); - initialContentParts.push(textBefore); - } - - const placeholderSpan = document.createElement('span'); - placeholderSpan.className = 'rich-text-placeholder'; - placeholderSpan.contentEditable = 'true'; - placeholderSpan.dataset.placeholderIndex = index.toString(); - placeholderSpan.dataset.placeholderName = placeholder.name; - - if (index === currentIndex) { - placeholderSpan.classList.add('rich-text-placeholder--active'); - } - - const displayText = placeholder.defaultValue || placeholder.name; - placeholderSpan.textContent = displayText; - initialContentParts.push(displayText); - - if (placeholder.description) { - placeholderSpan.title = placeholder.description; - } - - // Click to activate this placeholder. - placeholderSpan.addEventListener('click', (e) => { - e.stopPropagation(); - const clickedIndex = parseInt(placeholderSpan.dataset.placeholderIndex || '0', 10); - onUpdateCurrentIndex(clickedIndex); - selectPlaceholderByIndex(clickedIndex); - }); - - // Focus to activate this placeholder. - placeholderSpan.addEventListener('focus', () => { - const focusedIndex = parseInt(placeholderSpan.dataset.placeholderIndex || '0', 10); - onUpdateCurrentIndex(focusedIndex); - - editor.querySelectorAll('.rich-text-placeholder').forEach(el => { - el.classList.remove('rich-text-placeholder--active'); - }); - placeholderSpan.classList.add('rich-text-placeholder--active'); - }); - - // Keep input value in sync with placeholder edits. - placeholderSpan.addEventListener('input', () => { - const newContent = extractTemplateContent(); - onValueChange(newContent); - }); - - editor.appendChild(placeholderSpan); - lastIndex = placeholder.endIndex; - }); - - if (lastIndex < content.length) { - const textAfter = content.substring(lastIndex); - editor.appendChild(document.createTextNode(textAfter)); - initialContentParts.push(textAfter); - } - - const initialContent = initialContentParts.join(''); - onValueChange(initialContent); - }, [editorRef, extractTemplateContent, onValueChange, onUpdateCurrentIndex]); - - // Select a placeholder element by index. - const selectPlaceholderByIndex = useCallback((index: number) => { - if (!editorRef.current) return; - - const editor = editorRef.current as HTMLElement; - const placeholderElement = editor.querySelector( - `[data-placeholder-index="${index}"]` - ) as HTMLElement; - - if (!placeholderElement) { - log.warn('Placeholder element not found', { index }); - return; - } - - try { - editor.querySelectorAll('.rich-text-placeholder').forEach(el => { - el.classList.remove('rich-text-placeholder--active'); - }); - - placeholderElement.classList.add('rich-text-placeholder--active'); - - const range = document.createRange(); - const sel = window.getSelection(); - - range.selectNodeContents(placeholderElement); - sel?.removeAllRanges(); - sel?.addRange(range); - - placeholderElement.scrollIntoView({ - behavior: 'smooth', - block: 'nearest' - }); - } catch (error) { - log.error('Failed to select placeholder', { index, error }); - } - }, [editorRef]); - - // Handle template selection and optional fill mode. - const handleTemplateSelect = useCallback((template: PromptTemplate) => { - const placeholders = parseTemplate(template.content); - - if (placeholders.length > 0) { - onStartFill({ - currentIndex: 0, - placeholders, - filledValues: {}, - isActive: true - }); - - setTimeout(() => { - renderHighlightedTemplate(template.content, placeholders, 0); - - if (editorRef.current) { - editorRef.current.focus(); - selectPlaceholderByIndex(0); - } - }, 100); - } else { - onValueChange(template.content); - setTimeout(() => { - editorRef.current?.focus(); - }, 100); - } - }, [renderHighlightedTemplate, selectPlaceholderByIndex, onStartFill, onValueChange, editorRef]); - - // Exit fill mode and flatten placeholders to plain text. - const exitTemplateMode = useCallback(() => { - onExitFill(); - - if (editorRef.current) { - const editor = editorRef.current as HTMLElement; - const currentContent = extractTemplateContent(); - editor.textContent = currentContent; - - requestAnimationFrame(() => { - if (editor.childNodes.length > 0) { - const range = document.createRange(); - const sel = window.getSelection(); - range.selectNodeContents(editor); - range.collapse(false); - sel?.removeAllRanges(); - sel?.addRange(range); - } - editor.focus(); - }); - } - }, [extractTemplateContent, onExitFill, editorRef]); - - // Move to the next placeholder. - const moveToNextPlaceholder = useCallback(() => { - if (!templateFillState || !templateFillState.isActive) { - return false; - } - - const nextIndex = templateFillState.currentIndex + 1; - - if (nextIndex >= templateFillState.placeholders.length) { - exitTemplateMode(); - return false; - } - - onNextPlaceholder(); - selectPlaceholderByIndex(nextIndex); - return true; - }, [templateFillState, selectPlaceholderByIndex, exitTemplateMode, onNextPlaceholder]); - - // Move to the previous placeholder. - const moveToPrevPlaceholder = useCallback(() => { - if (!templateFillState || !templateFillState.isActive) { - return false; - } - - const prevIndex = templateFillState.currentIndex - 1; - - if (prevIndex < 0) { - return false; - } - - onPrevPlaceholder(); - selectPlaceholderByIndex(prevIndex); - return true; - }, [templateFillState, selectPlaceholderByIndex, onPrevPlaceholder]); - - return { - handleTemplateSelect, - exitTemplateMode, - moveToNextPlaceholder, - moveToPrevPlaceholder, - }; -} diff --git a/src/web-ui/src/flow_chat/reducers/templateReducer.ts b/src/web-ui/src/flow_chat/reducers/templateReducer.ts deleted file mode 100644 index 25da83b3..00000000 --- a/src/web-ui/src/flow_chat/reducers/templateReducer.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Template state reducer - */ - -import type { PlaceholderFillState } from '@/shared/types/prompt-template'; - -export interface TemplateState { - /** Template picker open state */ - isPickerOpen: boolean; - /** Current placeholder fill state */ - fillState: PlaceholderFillState | null; -} - -export type TemplateAction = - | { type: 'OPEN_PICKER' } - | { type: 'CLOSE_PICKER' } - | { type: 'TOGGLE_PICKER' } - | { type: 'START_FILL'; payload: PlaceholderFillState } - | { type: 'EXIT_FILL' } - | { type: 'UPDATE_CURRENT_INDEX'; payload: number } - | { type: 'NEXT_PLACEHOLDER' } - | { type: 'PREV_PLACEHOLDER' }; - -export const initialTemplateState: TemplateState = { - isPickerOpen: false, - fillState: null, -}; - -export function templateReducer(state: TemplateState, action: TemplateAction): TemplateState { - switch (action.type) { - case 'OPEN_PICKER': - return { ...state, isPickerOpen: true }; - - case 'CLOSE_PICKER': - return { ...state, isPickerOpen: false }; - - case 'TOGGLE_PICKER': - return { ...state, isPickerOpen: !state.isPickerOpen }; - - case 'START_FILL': - return { ...state, fillState: action.payload }; - - case 'EXIT_FILL': - return { ...state, fillState: null }; - - case 'UPDATE_CURRENT_INDEX': - if (!state.fillState) return state; - return { - ...state, - fillState: { - ...state.fillState, - currentIndex: action.payload, - }, - }; - - case 'NEXT_PLACEHOLDER': - if (!state.fillState) return state; - const nextIndex = state.fillState.currentIndex + 1; - if (nextIndex >= state.fillState.placeholders.length) { - // Reached the last placeholder; exit fill mode - return { ...state, fillState: null }; - } - return { - ...state, - fillState: { - ...state.fillState, - currentIndex: nextIndex, - }, - }; - - case 'PREV_PLACEHOLDER': - if (!state.fillState) return state; - const prevIndex = state.fillState.currentIndex - 1; - if (prevIndex < 0) return state; - return { - ...state, - fillState: { - ...state.fillState, - currentIndex: prevIndex, - }, - }; - - default: - return state; - } -} - diff --git a/src/web-ui/src/infrastructure/api/service-api/PromptTemplateAPI.ts b/src/web-ui/src/infrastructure/api/service-api/PromptTemplateAPI.ts deleted file mode 100644 index c1146ca6..00000000 --- a/src/web-ui/src/infrastructure/api/service-api/PromptTemplateAPI.ts +++ /dev/null @@ -1,56 +0,0 @@ - - -import { api } from './ApiClient'; -import { createTauriCommandError } from '../errors/TauriCommandError'; -import type { PromptTemplateConfig } from '@/shared/types/prompt-template'; - -export class PromptTemplateAPI { - - async getConfig(): Promise { - try { - return await api.invoke('get_prompt_template_config'); - } catch (error) { - throw createTauriCommandError('get_prompt_template_config', error); - } - } - - - async saveConfig(config: PromptTemplateConfig): Promise { - try { - await api.invoke('save_prompt_template_config', { config }); - } catch (error) { - throw createTauriCommandError('save_prompt_template_config', error, { config }); - } - } - - - async exportTemplates(): Promise { - try { - return await api.invoke('export_prompt_templates'); - } catch (error) { - throw createTauriCommandError('export_prompt_templates', error); - } - } - - - async importTemplates(json: string): Promise { - try { - await api.invoke('import_prompt_templates', { json }); - } catch (error) { - throw createTauriCommandError('import_prompt_templates', error, { json }); - } - } - - - async resetTemplates(): Promise { - try { - await api.invoke('reset_prompt_templates'); - } catch (error) { - throw createTauriCommandError('reset_prompt_templates', error); - } - } -} - - -export const promptTemplateAPI = new PromptTemplateAPI(); - diff --git a/src/web-ui/src/infrastructure/config/components/EditorConfig.tsx b/src/web-ui/src/infrastructure/config/components/EditorConfig.tsx index 2d1b59ff..91ecff01 100644 --- a/src/web-ui/src/infrastructure/config/components/EditorConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/EditorConfig.tsx @@ -3,7 +3,6 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { NumberInput, Select, Button, Switch, ConfigPageLoading, ConfigPageMessage } from '@/component-library'; -import { RotateCcw } from 'lucide-react'; import { configManager } from '../services/ConfigManager'; import { globalEventBus } from '@/infrastructure/event-bus'; import { DEFAULT_EDITOR_CONFIG, type EditorConfig as EditorConfigType, type EditorConfigPartial } from '@/tools/editor/config'; @@ -570,7 +569,7 @@ const EditorConfig: React.FC = () => {
@@ -580,7 +579,6 @@ const EditorConfig: React.FC = () => { onClick={resetConfig} disabled={isSaving} > - {t('actions.reset')} {isSaving && ( diff --git a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.scss b/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.scss deleted file mode 100644 index 47c0c25c..00000000 --- a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use '../../../component-library/styles/tokens' as *; - -.prompt-template-config { - &__shortcut-key { - display: inline-block; - padding: 1px 6px; - margin-left: $size-gap-2; - background: var(--element-bg-medium); - border: 1px solid var(--border-subtle); - border-radius: $size-radius-sm; - font-family: $font-family-mono; - font-size: $font-size-xs; - color: var(--color-text-primary); - } - - &__fav-badge { - display: inline-flex; - align-items: center; - flex-shrink: 0; - color: #f59e0b; - } - - &__modal-body { - display: flex; - flex-direction: column; - gap: $size-gap-3; - padding: $size-gap-4; - } - - &__modal-footer { - display: flex; - justify-content: flex-end; - gap: $size-gap-2; - padding: $size-gap-4; - border-top: 1px solid var(--border-subtle); - } -} diff --git a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx b/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx deleted file mode 100644 index e08f9463..00000000 --- a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Plus, Star, Edit2, Trash2, Copy, Download, Upload } from 'lucide-react'; -import { Button, IconButton, Modal, Input, Textarea, ConfirmDialog } from '@/component-library'; -import { ConfigPageLayout, ConfigPageHeader, ConfigPageContent, ConfigPageSection, ConfigCollectionItem } from './common'; -import { promptTemplateService } from '@/infrastructure/services/PromptTemplateService'; -import { notificationService } from '@/shared/notification-system'; -import type { PromptTemplate } from '@/shared/types/prompt-template'; -import { downloadDir, join } from '@tauri-apps/api/path'; -import { writeFile } from '@tauri-apps/plugin-fs'; -import './PromptTemplateConfig.scss'; - -export const PromptTemplateConfig: React.FC = () => { - const { t } = useTranslation('settings/prompt-templates'); - const [templates, setTemplates] = useState([]); - const [globalShortcut, setGlobalShortcut] = useState('Ctrl+Shift+P'); - const [isEditing, setIsEditing] = useState(false); - const [editingTemplate, setEditingTemplate] = useState(null); - const [expandedTemplateIds, setExpandedTemplateIds] = useState>(new Set()); - const [deleteConfirmId, setDeleteConfirmId] = useState(null); - - useEffect(() => { - const initializeAndLoad = async () => { - await promptTemplateService.initialize(); - loadTemplates(); - const config = promptTemplateService.getConfig(); - setGlobalShortcut(config.globalShortcut); - }; - initializeAndLoad(); - const unsubscribe = promptTemplateService.subscribe(() => { loadTemplates(); }); - return unsubscribe; - }, []); - - const loadTemplates = () => { - setTemplates(promptTemplateService.getAllTemplates()); - }; - - const sortedTemplates = [...templates].sort((a, b) => { - if (a.isFavorite && !b.isFavorite) return -1; - if (!a.isFavorite && b.isFavorite) return 1; - return b.usageCount - a.usageCount; - }); - - const handleCreateTemplate = () => { - setEditingTemplate({ - id: '', - name: '', - description: '', - content: '', - category: t('categories.uncategorized'), - isFavorite: false, - order: templates.length, - createdAt: Date.now(), - updatedAt: Date.now(), - usageCount: 0 - }); - setIsEditing(true); - }; - - const handleEditTemplate = (template: PromptTemplate, e: React.MouseEvent) => { - e.stopPropagation(); - setEditingTemplate({ ...template }); - setIsEditing(true); - }; - - const handleSaveTemplate = async () => { - if (!editingTemplate) return; - if (!editingTemplate.name.trim()) { notificationService.error(t('messages.nameRequired')); return; } - if (!editingTemplate.content.trim()) { notificationService.error(t('messages.contentRequired')); return; } - try { - if (editingTemplate.id) { - await promptTemplateService.updateTemplate(editingTemplate.id, editingTemplate); - notificationService.success(t('messages.templateUpdated')); - } else { - await promptTemplateService.createTemplate(editingTemplate); - notificationService.success(t('messages.templateCreated')); - } - setIsEditing(false); - setEditingTemplate(null); - loadTemplates(); - } catch (error) { - notificationService.error(t('messages.operationFailed', { error: (error as Error).message })); - } - }; - - const handleDeleteTemplate = async (id: string) => { - try { - await promptTemplateService.deleteTemplate(id); - notificationService.success(t('messages.templateDeleted')); - loadTemplates(); - } catch (error) { - notificationService.error(t('messages.deleteFailed', { error: (error as Error).message })); - } - }; - - const handleToggleFavorite = async (template: PromptTemplate, e: React.MouseEvent) => { - e.stopPropagation(); - try { - await promptTemplateService.updateTemplate(template.id, { isFavorite: !template.isFavorite }); - loadTemplates(); - } catch (error) { - notificationService.error(t('messages.operationFailed', { error: (error as Error).message })); - } - }; - - const handleDuplicateTemplate = async (template: PromptTemplate, e: React.MouseEvent) => { - e.stopPropagation(); - try { - await promptTemplateService.createTemplate({ - ...template, - name: `${template.name} (copy)`, - order: templates.length - }); - notificationService.success(t('messages.templateCopied')); - loadTemplates(); - } catch (error) { - notificationService.error(t('messages.copyFailed', { error: (error as Error).message })); - } - }; - - const handleExport = async () => { - try { - const json = await promptTemplateService.exportConfig(); - const fileName = `prompt-templates-${Date.now()}.json`; - const isTauri = typeof window !== 'undefined' && '__TAURI__' in window; - if (isTauri) { - try { - const downloadsPath = await downloadDir(); - const filePath = await join(downloadsPath, fileName); - const content = new TextEncoder().encode(json); - await writeFile(filePath, content); - notificationService.success(t('messages.configExported', { filePath })); - return; - } catch { - // fallback to browser download - } - } - const blob = new Blob([json], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - a.click(); - URL.revokeObjectURL(url); - notificationService.success(t('messages.configExported', { filePath: t('messages.defaultDownloadDir') })); - } catch (error) { - notificationService.error(t('messages.exportFailed', { error: (error as Error).message })); - } - }; - - const handleImport = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) return; - try { - const text = await file.text(); - if (await promptTemplateService.importConfig(text)) { - notificationService.success(t('messages.configImported')); - loadTemplates(); - } else { - notificationService.error(t('messages.importInvalid')); - } - } catch (error) { - notificationService.error(t('messages.importFailed', { error: (error as Error).message })); - } - }; - input.click(); - }; - - const toggleTemplateExpanded = (templateId: string) => { - setExpandedTemplateIds(prev => { - const next = new Set(prev); - if (next.has(templateId)) next.delete(templateId); - else next.add(templateId); - return next; - }); - }; - - const sectionExtra = ( - <> - - - - - - - - - - - ); - - return ( - - - - - - {t('section.templateList.description')} - {' '} - {t('shortcuts.openPickerReminder')} - {globalShortcut} - - } - extra={sectionExtra} - > - {sortedTemplates.length === 0 && ( -
- -
- )} - - {sortedTemplates.map(template => { - const badge = ( - <> - {template.category && ( - {template.category} - )} - {template.isFavorite && ( - - - - )} - - ); - - const control = ( - <> - handleToggleFavorite(template, e)} - tooltip={template.isFavorite ? t('actions.unfavorite') : t('actions.favorite')} - > - - - handleEditTemplate(template, e)} - tooltip={t('actions.edit')} - > - - - handleDuplicateTemplate(template, e)} - tooltip={t('actions.copy')} - > - - - { e.stopPropagation(); setDeleteConfirmId(template.id); }} - tooltip={t('actions.delete')} - > - - - - ); - - const details = ( - <> - {template.description && ( -
{template.description}
- )} -
-
{t('template.contentLabel')}
-
{template.content}
-
-
- {t('template.usageCount', { count: template.usageCount })} - {template.shortcut && ( - {template.shortcut} - )} -
- - ); - - return ( - toggleTemplateExpanded(template.id)} - /> - ); - })} -
-
- - setIsEditing(false)} - title={editingTemplate?.id ? t('modal.titleEdit') : t('modal.titleCreate')} - size="medium" - > - {editingTemplate && ( - <> -
- setEditingTemplate({ ...editingTemplate, name: e.target.value })} - placeholder={t('modal.fields.namePlaceholder')} - /> - setEditingTemplate({ ...editingTemplate, description: e.target.value })} - placeholder={t('modal.fields.descriptionPlaceholder')} - /> - setEditingTemplate({ ...editingTemplate, category: e.target.value })} - placeholder={t('modal.fields.categoryPlaceholder')} - /> -