diff --git a/.DS_Store b/.DS_Store index 2948516..10b0dbf 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/config/paths.py b/config/paths.py index 1fd835b..c19de5b 100644 --- a/config/paths.py +++ b/config/paths.py @@ -9,6 +9,7 @@ LOGS_DIR = "./logs" USER_SPACE_DIR = "./users" USERS_DB_FILE = f"{DATA_DIR}/users.json" INVITE_CODES_FILE = f"{DATA_DIR}/invite_codes.json" +ADMIN_POLICY_FILE = f"{DATA_DIR}/admin_policy.json" __all__ = [ "DEFAULT_PROJECT_PATH", @@ -18,4 +19,5 @@ __all__ = [ "USER_SPACE_DIR", "USERS_DB_FILE", "INVITE_CODES_FILE", + "ADMIN_POLICY_FILE", ] diff --git a/core/main_terminal.py b/core/main_terminal.py index 7ca1990..be9c384 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -137,13 +137,20 @@ class MainTerminal: self.focused_files = {} # {path: content} 存储聚焦的文件内容 self.current_session_id = 0 # 用于标识不同的任务会话 + # 工具类别(可被管理员动态覆盖) + self.tool_categories_map = dict(TOOL_CATEGORIES) + self.admin_forced_category_states: Dict[str, Optional[bool]] = {} + self.admin_disabled_models: List[str] = [] + self.admin_policy_ui_blocks: Dict[str, bool] = {} + self.admin_policy_version: Optional[str] = None + # 工具启用状态 - self.tool_category_states = { + self.tool_category_states: Dict[str, bool] = { key: category.default_enabled - for key, category in TOOL_CATEGORIES.items() + for key, category in self.tool_categories_map.items() } - self.disabled_tools = set() - self.disabled_notice_tools = set() + self.disabled_tools: Set[str] = set() + self.disabled_notice_tools: Set[str] = set() self._refresh_disabled_tools() self.thinking_fast_interval = THINKING_FAST_INTERVAL self.default_disabled_tool_categories: List[str] = [] @@ -358,12 +365,12 @@ class MainTerminal: if isinstance(raw_disabled, list): disabled_categories = [ key for key in raw_disabled - if isinstance(key, str) and key in TOOL_CATEGORIES + if isinstance(key, str) and key in self.tool_categories_map ] self.default_disabled_tool_categories = disabled_categories # Reset category states to defaults before applying overrides - for key, category in TOOL_CATEGORIES.items(): + for key, category in self.tool_categories_map.items(): self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled self._refresh_disabled_tools() @@ -539,30 +546,71 @@ class MainTerminal: def set_tool_category_enabled(self, category: str, enabled: bool) -> None: """设置工具类别的启用状态 / Toggle tool category enablement.""" - if category not in TOOL_CATEGORIES: + categories = self.tool_categories_map + if category not in categories: raise ValueError(f"未知的工具类别: {category}") + forced = self.admin_forced_category_states.get(category) + if isinstance(forced, bool) and forced != enabled: + raise ValueError("该类别被管理员强制为启用/禁用,无法修改") self.tool_category_states[category] = bool(enabled) self._refresh_disabled_tools() + def set_admin_policy( + self, + categories: Optional[Dict[str, "ToolCategory"]] = None, + forced_category_states: Optional[Dict[str, Optional[bool]]] = None, + disabled_models: Optional[List[str]] = None, + ) -> None: + """应用管理员策略(工具分类、强制开关、模型禁用)。""" + if categories: + self.tool_categories_map = dict(categories) + # 重新构建启用状态映射,保留已有值 + new_states: Dict[str, bool] = {} + for key, cat in self.tool_categories_map.items(): + if key in self.tool_category_states: + new_states[key] = self.tool_category_states[key] + else: + new_states[key] = cat.default_enabled + self.tool_category_states = new_states + # 清理已被移除的类别 + for removed in list(self.tool_category_states.keys()): + if removed not in self.tool_categories_map: + self.tool_category_states.pop(removed, None) + + self.admin_forced_category_states = forced_category_states or {} + self.admin_disabled_models = disabled_models or [] + self._refresh_disabled_tools() + def get_tool_settings_snapshot(self) -> List[Dict[str, object]]: """获取工具类别状态快照 / Return tool category states snapshot.""" snapshot: List[Dict[str, object]] = [] - for key, category in TOOL_CATEGORIES.items(): + categories = self.tool_categories_map + for key, category in categories.items(): + forced = self.admin_forced_category_states.get(key) + enabled = self.tool_category_states.get(key, category.default_enabled) + if isinstance(forced, bool): + enabled = forced snapshot.append({ "id": key, "label": category.label, - "enabled": self.tool_category_states.get(key, category.default_enabled), + "enabled": enabled, "tools": list(category.tools), + "locked": isinstance(forced, bool), + "locked_state": forced if isinstance(forced, bool) else None, }) return snapshot def _refresh_disabled_tools(self) -> None: """刷新禁用工具列表 / Refresh disabled tool set.""" - disabled = set() - notice = set() - for key, enabled in self.tool_category_states.items(): - if not enabled: - category = TOOL_CATEGORIES[key] + disabled: Set[str] = set() + notice: Set[str] = set() + categories = self.tool_categories_map + for key, category in categories.items(): + state = self.tool_category_states.get(key, category.default_enabled) + forced = self.admin_forced_category_states.get(key) + if isinstance(forced, bool): + state = forced + if not state: disabled.update(category.tools) if not getattr(category, "silent_when_disabled", False): notice.update(category.tools) diff --git a/modules/admin_policy_manager.py b/modules/admin_policy_manager.py new file mode 100644 index 0000000..ead2bcc --- /dev/null +++ b/modules/admin_policy_manager.py @@ -0,0 +1,228 @@ +"""管理员策略配置管理。 + +职责: +- 持久化管理员在工具分类、模型禁用、前端功能禁用等方面的策略。 +- 支持作用域:global / role / user / invite_code,按优先级合并得到最终生效策略。 +- 仅在此文件读写配置,避免侵入现有模块。 +""" + +from __future__ import annotations + +import json +import time +from pathlib import Path +from typing import Dict, Any, Tuple + +from config.paths import ADMIN_POLICY_FILE + +# 可用的模型 key(与前端、model_profiles 保持一致) +ALLOWED_MODELS = {"kimi", "deepseek", "qwen3-max", "qwen3-vl-plus"} + +# UI 禁用项键名,前后端统一 +UI_BLOCK_KEYS = [ + "collapse_workspace", + "block_file_manager", + "block_personal_space", + "block_upload", + "block_conversation_review", + "block_tool_toggle", + "block_realtime_terminal", + "block_focus_panel", + "block_token_panel", + "block_compress_conversation", + "block_virtual_monitor", +] + + +def _ensure_file() -> Path: + path = Path(ADMIN_POLICY_FILE).expanduser().resolve() + path.parent.mkdir(parents=True, exist_ok=True) + if not path.exists(): + _save_policy(_default_policy(), path) + return path + + +def _read_policy(path: Path) -> Dict[str, Any]: + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception: + return _default_policy() + + +def _save_policy(payload: Dict[str, Any], path: Path | None = None) -> None: + target = path or _ensure_file() + target.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + + +def _default_policy() -> Dict[str, Any]: + return { + "updated_at": time.strftime("%Y-%m-%d %H:%M:%S"), + "global": _blank_config(), + "roles": {}, + "users": {}, + "invites": {}, + } + + +def _blank_config() -> Dict[str, Any]: + return { + "category_overrides": {}, # id -> {label, tools, default_enabled?} + "remove_categories": [], # ids + "forced_category_states": {}, # id -> true/false + "disabled_models": [], # list of model keys + "ui_blocks": {}, # key -> bool + } + + +def _merge_config(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: + """浅合并策略,数组采取并集/覆盖逻辑。""" + merged = _blank_config() + base = base or {} + override = override or {} + + merged["category_overrides"] = { + **(base.get("category_overrides") or {}), + **(override.get("category_overrides") or {}), + } + merged["remove_categories"] = list({ + *[c for c in base.get("remove_categories") or [] if isinstance(c, str)], + *[c for c in override.get("remove_categories") or [] if isinstance(c, str)], + }) + merged["forced_category_states"] = { + **(base.get("forced_category_states") or {}), + **(override.get("forced_category_states") or {}), + } + merged["disabled_models"] = list({ + *[m for m in base.get("disabled_models") or [] if m in ALLOWED_MODELS], + *[m for m in override.get("disabled_models") or [] if m in ALLOWED_MODELS], + }) + ui_base = base.get("ui_blocks") or {} + ui_override = override.get("ui_blocks") or {} + merged["ui_blocks"] = {**ui_base, **ui_override} + return merged + + +def load_policy() -> Dict[str, Any]: + path = _ensure_file() + payload = _read_policy(path) + # 补全缺失字段 + if not isinstance(payload, dict): + payload = _default_policy() + payload.setdefault("updated_at", time.strftime("%Y-%m-%d %H:%M:%S")) + payload.setdefault("global", _blank_config()) + payload.setdefault("roles", {}) + payload.setdefault("users", {}) + payload.setdefault("invites", {}) + return payload + + +def save_scope_policy(target_type: str, target_value: str, config: Dict[str, Any]) -> Dict[str, Any]: + """更新指定作用域的策略并保存,返回最新策略。""" + if target_type not in {"global", "role", "user", "invite"}: + raise ValueError("invalid target_type") + + policy = load_policy() + normalized = _blank_config() + normalized = _merge_config(normalized, config or {}) + + if target_type == "global": + policy["global"] = normalized + elif target_type == "role": + policy.setdefault("roles", {})[target_value] = normalized + elif target_type == "user": + policy.setdefault("users", {})[target_value] = normalized + else: + policy.setdefault("invites", {})[target_value] = normalized + + policy["updated_at"] = time.strftime("%Y-%m-%d %H:%M:%S") + _save_policy(policy) + return policy + + +def _collect_categories_with_overrides(overrides: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: + """从 override 字典生成 {id: {label, tools, default_enabled}}""" + from core.tool_config import TOOL_CATEGORIES # 延迟导入避免循环 + + base: Dict[str, Dict[str, Any]] = { + key: { + "label": cat.label, + "tools": list(cat.tools), + "default_enabled": bool(cat.default_enabled), + "silent_when_disabled": getattr(cat, "silent_when_disabled", False), + } + for key, cat in TOOL_CATEGORIES.items() + } + + remove_ids = set(overrides.get("remove_categories") or []) + for rid in remove_ids: + base.pop(rid, None) + + for cid, payload in (overrides.get("category_overrides") or {}).items(): + if not isinstance(cid, str): + continue + if not isinstance(payload, dict): + continue + label = payload.get("label") or cid + tools = payload.get("tools") or [] + if not isinstance(tools, list): + continue + default_enabled = bool(payload.get("default_enabled", True)) + base[cid] = { + "label": str(label), + "tools": [t for t in tools if isinstance(t, str)], + "default_enabled": default_enabled, + "silent_when_disabled": bool(payload.get("silent_when_disabled", False)), + } + return base + + +def get_effective_policy(username: str | None, role: str | None, invite_code: str | None) -> Dict[str, Any]: + """按优先级合并策略,返回生效配置。""" + policy = load_policy() + scopes: Tuple[Tuple[str, Dict[str, Any]], ...] = ( + ("global", policy.get("global") or _blank_config()), + ("role", (policy.get("roles") or {}).get(role or "", {})), + ("invite", (policy.get("invites") or {}).get(invite_code or "", {})), + ("user", (policy.get("users") or {}).get(username or "", {})), + ) + + merged = _blank_config() + for _, cfg in scopes: + merged = _merge_config(merged, cfg or {}) + + # 计算最终分类 + categories = _collect_categories_with_overrides(merged) + forced_states = { + key: bool(val) if isinstance(val, bool) else None + for key, val in (merged.get("forced_category_states") or {}).items() + if key + } + disabled_models = [m for m in merged.get("disabled_models") or [] if m in ALLOWED_MODELS] + ui_blocks = {k: bool(v) for k, v in (merged.get("ui_blocks") or {}).items() if k in UI_BLOCK_KEYS} + + return { + "categories": categories, + "forced_category_states": forced_states, + "disabled_models": disabled_models, + "ui_blocks": ui_blocks, + "updated_at": policy.get("updated_at"), + } + + +def describe_defaults() -> Dict[str, Any]: + """返回默认(未覆盖)工具分类,用于前端渲染。""" + from core.tool_config import TOOL_CATEGORIES + + return { + "categories": { + key: { + "label": cat.label, + "tools": list(cat.tools), + "default_enabled": bool(cat.default_enabled), + "silent_when_disabled": getattr(cat, "silent_when_disabled", False), + } + for key, cat in TOOL_CATEGORIES.items() + }, + "models": sorted(list(ALLOWED_MODELS)), + "ui_block_keys": UI_BLOCK_KEYS, + } diff --git a/static/admin_policy/index.html b/static/admin_policy/index.html new file mode 100644 index 0000000..7e049b6 --- /dev/null +++ b/static/admin_policy/index.html @@ -0,0 +1,14 @@ + + + + + + 管理员策略配置 + + + + +
+ + + diff --git a/static/src/App.vue b/static/src/App.vue index cfbca65..c2b34ec 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -70,6 +70,7 @@ :is-connected="isConnected" :panel-menu-open="panelMenuOpen" :panel-mode="panelMode" + :file-manager-disabled="policyUiBlocks.block_file_manager" @toggle-panel-menu="togglePanelMenu" @select-panel="selectPanelMode" @open-file-manager="openGuiFileManager" @@ -187,6 +188,13 @@ :icon-style="iconStyle" :tool-category-icon="toolCategoryIcon" :selected-images="selectedImages" + :block-upload="policyUiBlocks.block_upload" + :block-tool-toggle="policyUiBlocks.block_tool_toggle" + :block-realtime-terminal="policyUiBlocks.block_realtime_terminal" + :block-focus-panel="policyUiBlocks.block_focus_panel" + :block-token-panel="policyUiBlocks.block_token_panel" + :block-compress-conversation="policyUiBlocks.block_compress_conversation" + :block-conversation-review="policyUiBlocks.block_conversation_review" @update:input-message="inputSetMessage" @input-change="handleInputChange" @input-focus="handleInputFocus" diff --git a/static/src/admin/AdminDashboardApp.vue b/static/src/admin/AdminDashboardApp.vue index 2700abb..3cab4c1 100644 --- a/static/src/admin/AdminDashboardApp.vue +++ b/static/src/admin/AdminDashboardApp.vue @@ -16,6 +16,7 @@ + 策略配置 @@ -557,6 +558,20 @@ const inviteStatus = (remaining: number | null | undefined) => { color: #5b4b35; } +.link-btn { + padding: 10px 14px; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + background: #fff; + color: #2a2013; + text-decoration: none; + font-weight: 600; +} + +.link-btn:hover { + background: rgba(0, 0, 0, 0.04); +} + button { border: none; border-radius: 999px; diff --git a/static/src/admin/PolicyApp.vue b/static/src/admin/PolicyApp.vue new file mode 100644 index 0000000..5c54049 --- /dev/null +++ b/static/src/admin/PolicyApp.vue @@ -0,0 +1,1074 @@ + + + + + diff --git a/static/src/admin/policyMain.ts b/static/src/admin/policyMain.ts new file mode 100644 index 0000000..4f97ea3 --- /dev/null +++ b/static/src/admin/policyMain.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import PolicyApp from './PolicyApp.vue'; + +createApp(PolicyApp).mount('#admin-policy-app'); diff --git a/static/src/app.ts b/static/src/app.ts index d37652f..6ae6b37 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -28,6 +28,7 @@ import { usePersonalizationStore } from './stores/personalization'; import { useModelStore } from './stores/model'; import { useChatActionStore } from './stores/chatActions'; import { useMonitorStore } from './stores/monitor'; +import { usePolicyStore } from './stores/policy'; import { ICONS, TOOL_CATEGORY_ICON_MAP } from './utils/icons'; import { initializeLegacySocket } from './composables/useLegacySocket'; import { useConnectionStore } from './stores/connection'; @@ -435,8 +436,21 @@ const appOptions = { } return this.thinkingMode ? 'thinking' : 'fast'; }, + policyUiBlocks() { + const store = usePolicyStore(); + return store.uiBlocks || {}; + }, + adminDisabledModels() { + const store = usePolicyStore(); + return store.disabledModelSet; + }, modelOptions() { - return this.models || []; + const disabledSet = this.adminDisabledModels || new Set(); + const options = this.models || []; + return options.map((opt) => ({ + ...opt, + disabled: disabledSet.has(opt.key) + })).filter((opt) => true); }, titleRibbonVisible() { return !this.isMobileViewport && this.chatDisplayMode === 'chat'; @@ -466,7 +480,7 @@ const appOptions = { }) , displayModeSwitchDisabled() { - return false; + return !!this.policyUiBlocks.block_virtual_monitor; }, displayLockEngaged() { return false; @@ -652,6 +666,28 @@ const appOptions = { } this.uiCloseMobileOverlay(); }, + applyPolicyUiLocks() { + const policyStore = usePolicyStore(); + const blocks = policyStore.uiBlocks; + if (blocks.collapse_workspace) { + this.uiSetWorkspaceCollapsed(true); + } + if (blocks.block_virtual_monitor && this.chatDisplayMode === 'monitor') { + this.uiSetChatDisplayMode('chat'); + } + }, + isPolicyBlocked(key: string, message?: string) { + const policyStore = usePolicyStore(); + if (policyStore.uiBlocks[key]) { + this.uiPushToast({ + title: '已被管理员禁用', + message: message || '被管理员强制禁用', + type: 'warning' + }); + return true; + } + return false; + }, handleClickOutsideMobileMenu(event) { if (!this.isMobileViewport || !this.mobileOverlayMenuOpen) { return; @@ -666,6 +702,10 @@ const appOptions = { if (this.isMobileViewport) { return; } + if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) { + this.uiSetWorkspaceCollapsed(true); + return; + } const nextState = !this.workspaceCollapsed; this.uiSetWorkspaceCollapsed(nextState); if (nextState) { @@ -674,6 +714,11 @@ const appOptions = { }, handleDisplayModeToggle() { if (this.displayModeSwitchDisabled) { + // 显式提示管理员禁用 + this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用'); + return; + } + if (this.chatDisplayMode === 'chat' && this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')) { return; } const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat'; @@ -912,6 +957,9 @@ const appOptions = { }, openGuiFileManager() { + if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) { + return; + } window.open('/file-manager', '_blank'); }, @@ -1393,6 +1441,10 @@ const appOptions = { this.applyStatusSnapshot(statusData); // 立即更新配额和运行模式,避免等待其他慢接口 this.fetchUsageQuota(); + // 拉取管理员策略 + const policyStore = usePolicyStore(); + await policyStore.fetchPolicy(); + this.applyPolicyUiLocks(); // 获取当前对话信息 const statusConversationId = statusData.conversation && statusData.conversation.current_id; @@ -2270,6 +2322,9 @@ const appOptions = { }, openPersonalPage() { + if (this.isPolicyBlocked('block_personal_space', '个人空间已被管理员禁用')) { + return; + } this.personalizationOpenDrawer(); }, @@ -2303,6 +2358,15 @@ const appOptions = { }, handleFileSelected(files) { + const policyStore = usePolicyStore(); + if (policyStore.uiBlocks?.block_upload) { + this.uiPushToast({ + title: '上传被禁用', + message: '已被管理员禁用上传功能', + type: 'warning' + }); + return; + } this.uploadHandleSelected(files); }, @@ -2528,6 +2592,9 @@ const appOptions = { if (!this.isConnected) { return; } + if (this.isPolicyBlocked('block_tool_toggle', '工具启用/禁用已被管理员锁定')) { + return; + } this.modeMenuOpen = false; this.modelMenuOpen = false; const nextState = this.inputToggleToolMenu(); @@ -2666,6 +2733,9 @@ const appOptions = { if (this.uploading || !this.isConnected) { return; } + if (this.isPolicyBlocked('block_upload', '上传功能已被管理员禁用')) { + return; + } this.triggerFileUpload(); }, @@ -2714,6 +2784,15 @@ const appOptions = { if (!this.isConnected || this.streamingMessage) { return; } + const policyStore = usePolicyStore(); + if (policyStore.isModelDisabled(key)) { + this.uiPushToast({ + title: '模型被禁用', + message: '被管理员强制禁用', + type: 'warning' + }); + return; + } if (this.conversationHasImages && key !== 'qwen3-vl-plus') { this.uiPushToast({ title: '切换失败', @@ -2869,6 +2948,9 @@ const appOptions = { if (!this.isConnected) { return; } + if (this.isPolicyBlocked('block_realtime_terminal', '实时终端已被管理员禁用')) { + return; + } this.openRealtimeTerminal(); }, @@ -2876,6 +2958,9 @@ const appOptions = { if (!this.isConnected) { return; } + if (this.isPolicyBlocked('block_focus_panel', '聚焦面板已被管理员禁用')) { + return; + } this.toggleFocusPanel(); }, @@ -2883,6 +2968,9 @@ const appOptions = { if (!this.currentConversationId) { return; } + if (this.isPolicyBlocked('block_token_panel', '用量统计已被管理员禁用')) { + return; + } this.toggleTokenPanel(); }, @@ -2890,10 +2978,16 @@ const appOptions = { if (this.compressing || this.streamingMessage || !this.isConnected) { return; } + if (this.isPolicyBlocked('block_compress_conversation', '压缩对话已被管理员禁用')) { + return; + } this.compressConversation(); }, openReviewDialog() { + if (this.isPolicyBlocked('block_conversation_review', '对话引用已被管理员禁用')) { + return; + } if (!this.isConnected) { this.uiPushToast({ title: '无法使用', @@ -3165,7 +3259,9 @@ const appOptions = { id: item.id, label: item.label || item.id, enabled: !!item.enabled, - tools: Array.isArray(item.tools) ? item.tools : [] + tools: Array.isArray(item.tools) ? item.tools : [], + locked: !!item.locked, + locked_state: typeof item.locked_state === 'boolean' ? item.locked_state : null })); debugLog('[ToolSettings] Snapshot applied', { received: categories.length, @@ -3215,6 +3311,15 @@ const appOptions = { if (this.toolSettingsLoading) { return; } + const policyStore = usePolicyStore(); + if (policyStore.isCategoryLocked(categoryId)) { + this.uiPushToast({ + title: '无法修改', + message: '该工具类别被管理员强制设置', + type: 'warning' + }); + return; + } const previousSnapshot = this.toolSettings.map((item) => ({ ...item })); const updatedSettings = this.toolSettings.map((item) => { if (item.id === categoryId) { @@ -3239,6 +3344,13 @@ const appOptions = { this.applyToolSettingsSnapshot(data.categories); } else { console.warn('更新工具设置失败:', data); + if (data && (data.message || data.error)) { + this.uiPushToast({ + title: '无法切换工具', + message: data.message || data.error, + type: 'warning' + }); + } this.toolSetSettings(previousSnapshot); } } catch (error) { diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index a2c8ffc..abac9c8 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -74,6 +74,13 @@ :current-conversation-id="currentConversationId" :icon-style="iconStyle" :tool-category-icon="toolCategoryIcon" + :block-upload="blockUpload" + :block-tool-toggle="blockToolToggle" + :block-realtime-terminal="blockRealtimeTerminal" + :block-focus-panel="blockFocusPanel" + :block-token-panel="blockTokenPanel" + :block-compress-conversation="blockCompressConversation" + :block-conversation-review="blockConversationReview" @quick-upload="triggerQuickUpload" @pick-images="$emit('pick-images')" @toggle-tool-menu="$emit('toggle-tool-menu')" @@ -147,9 +154,16 @@ const props = defineProps<{ currentConversationId: string | null; iconStyle: (key: string) => Record; toolCategoryIcon: (categoryId: string) => string; - modelOptions: Array<{ key: string; label: string; description: string }>; + modelOptions: Array<{ key: string; label: string; description: string; disabled?: boolean }>; currentModelKey: string; selectedImages?: string[]; + blockUpload?: boolean; + blockToolToggle?: boolean; + blockRealtimeTerminal?: boolean; + blockFocusPanel?: boolean; + blockTokenPanel?: boolean; + blockCompressConversation?: boolean; + blockConversationReview?: boolean; }>(); const inputStore = useInputStore(); diff --git a/static/src/components/input/QuickMenu.vue b/static/src/components/input/QuickMenu.vue index 979f97d..7f891e9 100644 --- a/static/src/components/input/QuickMenu.vue +++ b/static/src/components/input/QuickMenu.vue @@ -1,7 +1,12 @@