foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
/* 所有请求走相对路径:dev 由 vite proxy 透到 8000,prod 与后端同源 —— 无需区分环境。 */
|
|
2
1
|
export const API = '/api'
|
|
3
2
|
|
|
4
|
-
/* FastAPI 错误统一带 {detail},直接抛出来比 "xxx -> 400" 有用得多。
|
|
5
|
-
detail 可能是字符串,也可能是结构化对象({error:"install_denied",reasons:[...]} /
|
|
6
|
-
{needs_ack:true,warnings:[...]})—— 直接 String() 会得到 "[object Object]",
|
|
7
|
-
所以拆一层;原始 detail / status 挂到 err 上,调用方要区分「要确认」才有据可依。 */
|
|
8
3
|
function detailMsg(d) {
|
|
9
4
|
if (typeof d === 'string') return d
|
|
10
5
|
if (d && typeof d === 'object') {
|
|
@@ -74,8 +69,6 @@ export async function postForm(path, form) {
|
|
|
74
69
|
return r.json()
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
/* 少数端点在根路径、不带 /api 前缀 —— vendor/hkc 的 knowledge router 是直接
|
|
78
|
-
include_router(knowledge.router) 挂上去的,实测 /api/knowledge/* 是 404。 */
|
|
79
72
|
export async function getRaw(path, init) {
|
|
80
73
|
const r = await fetch(path, init)
|
|
81
74
|
if (!r.ok) throw await toError(r, path)
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
runSearch, say, send, setMode, stop,
|
|
10
10
|
} from '../stores/chat.js'
|
|
11
11
|
import { setModule } from '../stores/app.js'
|
|
12
|
+
import { t } from '../lib/i18n.js'
|
|
12
13
|
|
|
13
14
|
const text = ref('')
|
|
14
15
|
const ta = ref(null)
|
|
@@ -17,29 +18,31 @@ const mq = ref('')
|
|
|
17
18
|
const hover = ref(0)
|
|
18
19
|
const skills = ref([])
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ key: '
|
|
23
|
-
{ key: '
|
|
24
|
-
{ key: '
|
|
25
|
-
{ key: '
|
|
26
|
-
{ key: '
|
|
27
|
-
{ key: '
|
|
28
|
-
{ key: '
|
|
21
|
+
// 命令字面量保持中文(输入语法不变),只有说明文字跟随界面语言
|
|
22
|
+
const CMD = computed(() => [
|
|
23
|
+
{ key: '/新建', desc: t('chat.cmd.new'), run: () => newSession() },
|
|
24
|
+
{ key: '/流式', desc: t('chat.cmd.stream'), run: () => { setMode('stream'); say(t('chat.saidStream')) } },
|
|
25
|
+
{ key: '/任务', desc: t('chat.cmd.orchestrate'), run: () => { setMode('orchestrate'); say(t('chat.saidOrchestrate')) } },
|
|
26
|
+
{ key: '/搜索', desc: t('chat.cmd.search'), arg: true },
|
|
27
|
+
{ key: '/摄入', desc: t('chat.cmd.ingest'), run: () => requestIngest() },
|
|
28
|
+
{ key: '/设置', desc: t('chat.cmd.settings'), run: () => setModule('settings') },
|
|
29
|
+
{ key: '/停止', desc: t('chat.cmd.stop'), run: () => stop() },
|
|
30
|
+
{ key: '/清屏', desc: t('chat.cmd.clear'), run: () => { chat.messages = [] } },
|
|
29
31
|
{
|
|
30
32
|
key: '/帮助',
|
|
31
|
-
desc: '
|
|
32
|
-
run: () => pushLocal(CMD.map((c) => `${c.key.padEnd(6)} ${c.desc}`).join('\n')),
|
|
33
|
+
desc: t('chat.cmd.help'),
|
|
34
|
+
run: () => pushLocal(CMD.value.map((c) => `${c.key.padEnd(6)} ${c.desc}`).join('\n')),
|
|
33
35
|
},
|
|
34
|
-
]
|
|
36
|
+
])
|
|
35
37
|
|
|
36
38
|
const busy = () => chat.streamState === 'streaming'
|
|
37
39
|
|
|
38
40
|
const items = computed(() => {
|
|
39
41
|
const q = mq.value.toLowerCase()
|
|
40
42
|
if (menu.value === 'slash') {
|
|
41
|
-
return CMD.
|
|
42
|
-
.
|
|
43
|
+
return CMD.value
|
|
44
|
+
.filter((c) => c.key.slice(1).toLowerCase().includes(q) || c.desc.includes(mq.value))
|
|
45
|
+
.map((c) => ({ label: c.key, desc: c.desc, tag: c.arg ? t('chat.tagArg') : '' }))
|
|
43
46
|
}
|
|
44
47
|
if (menu.value === 'at') {
|
|
45
48
|
return skills.value
|
|
@@ -49,7 +52,7 @@ const items = computed(() => {
|
|
|
49
52
|
return []
|
|
50
53
|
})
|
|
51
54
|
|
|
52
|
-
const hint = computed(() => (menu.value === 'slash' ? '
|
|
55
|
+
const hint = computed(() => (menu.value === 'slash' ? t('chat.hintSlash') : t('chat.hintAt')))
|
|
53
56
|
|
|
54
57
|
onMounted(async () => {
|
|
55
58
|
grow()
|
|
@@ -68,13 +71,11 @@ onMounted(async () => {
|
|
|
68
71
|
})),
|
|
69
72
|
]
|
|
70
73
|
} catch {
|
|
71
|
-
/* 拿不到就只留命令面板 */
|
|
72
74
|
}
|
|
73
75
|
})
|
|
74
76
|
|
|
75
77
|
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
|
76
78
|
|
|
77
|
-
/* 只看光标左侧的 token:用户在中间补字时也能正确触发 */
|
|
78
79
|
function syncMenu() {
|
|
79
80
|
const el = ta.value
|
|
80
81
|
if (!el) return
|
|
@@ -113,7 +114,6 @@ function choose(i) {
|
|
|
113
114
|
if (menu.value === 'slash') {
|
|
114
115
|
const cmd = CMD.find((c) => c.key === it.label)
|
|
115
116
|
if (cmd && cmd.arg) {
|
|
116
|
-
/* 带参数的命令:把命令留在输入框,让用户接着写关键词 */
|
|
117
117
|
text.value = `${cmd.key} `
|
|
118
118
|
closeMenu()
|
|
119
119
|
nextTick(() => { el && el.focus(); grow() })
|
|
@@ -126,7 +126,6 @@ function choose(i) {
|
|
|
126
126
|
return
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/* @ 技能:把当前 @token 替换成完整技能名 */
|
|
130
129
|
const idx = before.lastIndexOf('@')
|
|
131
130
|
const after = text.value.slice(pos)
|
|
132
131
|
text.value = `${before.slice(0, idx)}@${it.label} ${after}`
|
|
@@ -134,13 +133,12 @@ function choose(i) {
|
|
|
134
133
|
nextTick(() => { el && el.focus(); grow() })
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
const cmd = CMD.find((c) => c.key === head)
|
|
136
|
+
function tryCommand(raw) {
|
|
137
|
+
if (!raw.startsWith('/')) return false
|
|
138
|
+
const [head, ...rest] = raw.split(/\s+/)
|
|
139
|
+
const cmd = CMD.value.find((c) => c.key === head)
|
|
142
140
|
if (!cmd) {
|
|
143
|
-
say(
|
|
141
|
+
say(t('chat.unknownCmd', { head }))
|
|
144
142
|
return true
|
|
145
143
|
}
|
|
146
144
|
const arg = rest.join(' ').trim()
|
|
@@ -160,8 +158,6 @@ function submit() {
|
|
|
160
158
|
send(t)
|
|
161
159
|
}
|
|
162
160
|
|
|
163
|
-
/* textarea 高度:聚焦即撑开(约 3 行),内容变多继续长高,到上限才转内部滚动。
|
|
164
|
-
上限跟视口走,窄屏/高分屏都不会把消息流挤没。 */
|
|
165
161
|
const FOCUS_MIN = 84
|
|
166
162
|
const focused = ref(false)
|
|
167
163
|
function capH() {
|
|
@@ -175,7 +171,6 @@ function grow() {
|
|
|
175
171
|
const min = focused.value ? FOCUS_MIN : 0
|
|
176
172
|
el.style.height = `${Math.min(Math.max(need, min), capH())}px`
|
|
177
173
|
}
|
|
178
|
-
/* blur 时 document.activeElement 已变,不能用它判断,单独跟一个标志 */
|
|
179
174
|
function onFocus() {
|
|
180
175
|
focused.value = true
|
|
181
176
|
grow()
|
|
@@ -240,8 +235,8 @@ function kb(n) {
|
|
|
240
235
|
<div v-if="chat.ingest" class="chip ing" :class="chat.ingest.status">
|
|
241
236
|
<span class="cn">{{ chat.ingest.name }}</span>
|
|
242
237
|
<span class="cs">
|
|
243
|
-
{{ chat.ingest.status === 'done' ?
|
|
244
|
-
: chat.ingest.status === 'error' ? '
|
|
238
|
+
{{ chat.ingest.status === 'done' ? t('common.count', { n: chat.ingest.ku })
|
|
239
|
+
: chat.ingest.status === 'error' ? t('chat.ingestFailed')
|
|
245
240
|
: `${chat.ingest.pct}%` }}
|
|
246
241
|
</span>
|
|
247
242
|
<span class="bar"><i :style="{ width: `${chat.ingest.status === 'done' ? 100 : chat.ingest.pct}%` }" /></span>
|
|
@@ -265,7 +260,7 @@ function kb(n) {
|
|
|
265
260
|
ref="ta"
|
|
266
261
|
v-model="text"
|
|
267
262
|
rows="1"
|
|
268
|
-
placeholder="
|
|
263
|
+
:placeholder="t('chat.placeholder')"
|
|
269
264
|
@keydown="onKey"
|
|
270
265
|
@input="grow"
|
|
271
266
|
@focus="onFocus"
|
|
@@ -273,7 +268,7 @@ function kb(n) {
|
|
|
273
268
|
@click="syncMenu"
|
|
274
269
|
@keyup="syncMenu"
|
|
275
270
|
/>
|
|
276
|
-
<button class="send" :class="{ stop: busy() }" :title="busy() ? '
|
|
271
|
+
<button class="send" :class="{ stop: busy() }" :title="busy() ? t('chat.stopGen') : t('chat.send')" @click="submit">
|
|
277
272
|
{{ busy() ? '■' : '➤' }}
|
|
278
273
|
</button>
|
|
279
274
|
</div>
|
|
@@ -288,10 +283,6 @@ function kb(n) {
|
|
|
288
283
|
<style scoped>
|
|
289
284
|
.dock { padding: 0 8px 8px; position: relative; }
|
|
290
285
|
|
|
291
|
-
/* 输入框宽度 = 正文列宽。正文列是 .ms-in(max-width 780,左右 padding 22)居中于
|
|
292
|
-
.stage-body(margin 0 8px),这里用同一个算式 100% - 44:
|
|
293
|
-
宽屏时同为 736,窄屏时同为 列宽-60,任何宽度下左右边都压在正文的文字边上。
|
|
294
|
-
菜单 / 提示条挂在这层里,才会跟着一起变窄。 */
|
|
295
286
|
.dock-in {
|
|
296
287
|
position: relative;
|
|
297
288
|
width: min(736px, calc(100% - 44px));
|
|
@@ -342,13 +333,10 @@ textarea {
|
|
|
342
333
|
font-size: 13.5px; line-height: 1.6;
|
|
343
334
|
padding: 5px 2px; border: 0; background: none;
|
|
344
335
|
resize: none; overflow-y: auto;
|
|
345
|
-
/* 高度由 grow() 写死具体 px;max-height 只作兜底,避免 JS 失效时无限长高 */
|
|
346
336
|
max-height: 340px;
|
|
347
337
|
transition: height 0.14s ease;
|
|
348
338
|
}
|
|
349
339
|
textarea:focus { outline: none; }
|
|
350
|
-
/* placeholder 是「第一次进来唯一能读的提示」,用 dim 而不是 faint:
|
|
351
|
-
输入框是视觉焦点,提示跟着提亮一档才不会被当成禁用态。 */
|
|
352
340
|
textarea::placeholder { color: var(--dim); opacity: 1; }
|
|
353
341
|
|
|
354
342
|
.send {
|
|
@@ -10,13 +10,10 @@ const lines = computed(() =>
|
|
|
10
10
|
(props.doc.content || '').replace(/\n$/, '').split('\n')
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
/* 文字版正文走 lib/md.js 的共用渲染器:技能 SKILL.md 与记忆摘要都带
|
|
14
|
-
# / ** / - / > / ```,这里原来自带一套只认前四样,代码块原样倒出来。 */
|
|
15
13
|
const readHTML = computed(() => mdToHTML(props.doc.content))
|
|
16
14
|
</script>
|
|
17
15
|
|
|
18
16
|
<template>
|
|
19
|
-
<!-- 代码 → CLI 皮肤:暗色、等宽、行号、横向滚动。整栏不变暗,只有正文块变暗。 -->
|
|
20
17
|
<div v-if="doc.kind === 'code'" class="cli">
|
|
21
18
|
<div class="term-bar">
|
|
22
19
|
<span class="tl" /><span class="tl" /><span class="tl" />
|
|
@@ -31,7 +28,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
|
|
|
31
28
|
</div>
|
|
32
29
|
</div>
|
|
33
30
|
|
|
34
|
-
<!-- 文字 → 阅读版:衬线正文、68ch 行宽、留白优先 -->
|
|
35
31
|
<article v-else class="read scroll">
|
|
36
32
|
<header class="read-head">
|
|
37
33
|
<h2>{{ doc.title }}</h2>
|
|
@@ -45,7 +41,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
|
|
|
45
41
|
</template>
|
|
46
42
|
|
|
47
43
|
<style scoped>
|
|
48
|
-
/* ═══ CLI 皮肤 ═══ */
|
|
49
44
|
.cli {
|
|
50
45
|
flex: 1; min-width: 0; display: flex; flex-direction: column;
|
|
51
46
|
margin: 8px; border-radius: var(--r-mid); overflow: hidden;
|
|
@@ -87,7 +82,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
|
|
|
87
82
|
}
|
|
88
83
|
.code { color: var(--cli-ink); padding-right: 24px; }
|
|
89
84
|
|
|
90
|
-
/* ═══ 阅读皮肤 ═══ */
|
|
91
85
|
.read { flex: 1; overflow-y: auto; padding: 34px 8px 60px; }
|
|
92
86
|
.read-head, .read-body { max-width: 68ch; margin: 0 auto; }
|
|
93
87
|
.read-head { padding: 0 20px 18px; border-bottom: 1px solid var(--line-soft); }
|
|
@@ -101,7 +95,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
|
|
|
101
95
|
}
|
|
102
96
|
.read-meta code { font-family: var(--mono); font-size: 11.5px; color: var(--ink); }
|
|
103
97
|
.read-body { padding: 20px 20px 0; }
|
|
104
|
-
/* 正文是 v-html 生成的,没有 scoped 属性,必须 :deep */
|
|
105
98
|
.read-body :deep(p) {
|
|
106
99
|
font-family: var(--serif);
|
|
107
100
|
font-size: 15px; line-height: 1.92; color: #24302a;
|
|
@@ -149,7 +142,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
|
|
|
149
142
|
margin: 26px 0 10px;
|
|
150
143
|
}
|
|
151
144
|
|
|
152
|
-
/* 代码块:md.js 现在真会把 ``` 渲染成 <pre><code>,以前是整段原样倒出来 */
|
|
153
145
|
.read-body :deep(pre) {
|
|
154
146
|
margin: 0 0 15px; padding: 11px 13px;
|
|
155
147
|
background: var(--cli-bg); border-radius: var(--r-sm);
|
|
@@ -160,7 +152,6 @@ const readHTML = computed(() => mdToHTML(props.doc.content))
|
|
|
160
152
|
font-size: 12px; line-height: 1.72;
|
|
161
153
|
}
|
|
162
154
|
|
|
163
|
-
/* 表格 */
|
|
164
155
|
.read-body :deep(table) { width: 100%; border-collapse: collapse; margin: 0 0 15px; }
|
|
165
156
|
.read-body :deep(th),
|
|
166
157
|
.read-body :deep(td) {
|
|
@@ -5,7 +5,6 @@ import { state, loadProgress, closeDoc, setModule } from '../stores/app.js'
|
|
|
5
5
|
import { panels, loadPanel, openKu, startReview } from '../stores/panels.js'
|
|
6
6
|
import { fmtTime } from '../lib/fmt.js'
|
|
7
7
|
|
|
8
|
-
/* 右栏三块数据与对应面板共用同一份,进来就保证已加载 */
|
|
9
8
|
onMounted(() => {
|
|
10
9
|
loadProgress()
|
|
11
10
|
loadPanel('knowledge')
|
|
@@ -16,7 +15,6 @@ const doc = computed(() => state.doc)
|
|
|
16
15
|
const kb = panels.knowledge
|
|
17
16
|
const rv = panels.review
|
|
18
17
|
|
|
19
|
-
/* 今日待办 = 到期 + 新卡(SM-2 队列就是这个口径) */
|
|
20
18
|
const reviewTodo = computed(() => (rv.stats.due || 0) + (rv.stats.fresh || 0))
|
|
21
19
|
const masteryPct = computed(() => Math.round((rv.stats.avg_mastery || 0) * 100))
|
|
22
20
|
|
|
@@ -26,7 +24,6 @@ function goReview() {
|
|
|
26
24
|
startReview()
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
/* 右栏只留 10 条,多的走「更多」到知识库面板看全量 */
|
|
30
27
|
const LIMIT = 10
|
|
31
28
|
const recentKb = computed(() => kb.recent.slice(0, LIMIT))
|
|
32
29
|
const kbMore = computed(() => kb.recentTotal > recentKb.value.length)
|
|
@@ -36,7 +33,6 @@ function moreKb() {
|
|
|
36
33
|
setModule('knowledge')
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
/* 大纲:代码取顶层定义,文字取段落首句。P2 换成真解析 */
|
|
40
36
|
const outline = computed(() => {
|
|
41
37
|
const d = doc.value
|
|
42
38
|
if (!d) return []
|
|
@@ -164,7 +160,6 @@ const outline = computed(() => {
|
|
|
164
160
|
}
|
|
165
161
|
.kpi div {
|
|
166
162
|
padding: 8px 9px; border-radius: var(--r-sm);
|
|
167
|
-
/* 纸面上要的是「白卡」而不是「更浅的纸」:0.72 的白压在暖底上几乎看不出边界 */
|
|
168
163
|
background: rgba(255, 255, 255, 0.9);
|
|
169
164
|
border: 1px solid var(--line);
|
|
170
165
|
}
|
|
@@ -196,7 +191,6 @@ const outline = computed(() => {
|
|
|
196
191
|
.row b { font-size: 13px; font-weight: 600; color: var(--w-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
197
192
|
.row span { font-size: 11.5px; color: var(--w-dim); }
|
|
198
193
|
|
|
199
|
-
/* 「更多」:三条杠 + 文字,点进知识库看全量 */
|
|
200
194
|
.more {
|
|
201
195
|
width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
202
196
|
margin-top: 4px; padding: 7px 9px;
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
3
3
|
import MenuPopover from './MenuPopover.vue'
|
|
4
4
|
import {
|
|
5
|
-
state, loadSessions, openSession, moduleLabel,
|
|
5
|
+
state, loadSessions, openSession, moduleLabel, moduleIcon, setModule,
|
|
6
|
+
railItems, moreItems, PINNED, RAIL,
|
|
6
7
|
} from '../stores/app.js'
|
|
7
8
|
import { deleteSession, loadSession, newSession, renameSession } from '../stores/chat.js'
|
|
9
|
+
import { numberLocale, t } from '../lib/i18n.js'
|
|
8
10
|
|
|
9
11
|
const btnRef = ref(null)
|
|
10
12
|
const menuOpen = ref(false)
|
|
@@ -13,13 +15,10 @@ const anchor = ref(null)
|
|
|
13
15
|
const count = computed(() => state.sessions.length)
|
|
14
16
|
const rail = railItems()
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const isMore = computed(() => !RAIL.includes(state.module))
|
|
18
|
+
const pinnedItems = PINNED.map((id) => ({ id, icon: moduleIcon(id) }))
|
|
19
|
+
const isMore = computed(() => moreItems().some((m) => m.id === state.module))
|
|
19
20
|
const moreCur = computed(() => (isMore.value && state.module !== 'overview' ? moduleLabel(state.module) : ''))
|
|
20
21
|
|
|
21
|
-
/* 线性图标:统一 24 视框 + 描边,和右栏 / 顶栏的图形语言一致。
|
|
22
|
-
只放 path 的 d,颜色与线宽交给 CSS,换肤时不用动 JS。 */
|
|
23
22
|
const ICONS = {
|
|
24
23
|
book: [
|
|
25
24
|
'M5 5.6A2.1 2.1 0 0 1 7.1 3.5H19v17H7.1A2.1 2.1 0 0 1 5 18.4z',
|
|
@@ -43,6 +42,17 @@ const ICONS = {
|
|
|
43
42
|
'M6.1 21.4v-3.6h3.6',
|
|
44
43
|
],
|
|
45
44
|
memory: ['M7.2 3.4h9.6v17.2L12 17.2l-4.8 3.4z'],
|
|
45
|
+
gear: [
|
|
46
|
+
'M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4z',
|
|
47
|
+
'M12 3.1v1.9',
|
|
48
|
+
'M12 19v1.9',
|
|
49
|
+
'M3.1 12h1.9',
|
|
50
|
+
'M19 12h1.9',
|
|
51
|
+
'M5.7 5.7l1.4 1.4',
|
|
52
|
+
'M16.9 16.9l1.4 1.4',
|
|
53
|
+
'M18.3 5.7l-1.4 1.4',
|
|
54
|
+
'M7.1 16.9l-1.4 1.4',
|
|
55
|
+
],
|
|
46
56
|
dot: ['M12 10.6a1.4 1.4 0 1 0 0 2.8 1.4 1.4 0 0 0 0-2.8z'],
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -60,7 +70,6 @@ function toggleMenu() {
|
|
|
60
70
|
return
|
|
61
71
|
}
|
|
62
72
|
const r = btnRef.value?.getBoundingClientRect()
|
|
63
|
-
/* 入口现在在栏顶,浮层必须往下长(原来的实现是贴着栏底往上长) */
|
|
64
73
|
if (r) anchor.value = { left: r.left, bottom: r.bottom }
|
|
65
74
|
menuOpen.value = true
|
|
66
75
|
}
|
|
@@ -70,13 +79,11 @@ function pick(id) {
|
|
|
70
79
|
menuOpen.value = false
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
/* 切会话 = 切左侧高亮 + 拉后端历史消息 */
|
|
74
82
|
function pickSession(s) {
|
|
75
83
|
openSession(s)
|
|
76
84
|
loadSession(s.id)
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
/* ── 会话重命名 / 删除 ── */
|
|
80
87
|
const editingId = ref('')
|
|
81
88
|
const draft = ref('')
|
|
82
89
|
let editEl = null
|
|
@@ -94,19 +101,17 @@ function cancelRename() {
|
|
|
94
101
|
editingId.value = ''
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
/* enter / blur 都会走到这里:先清 editingId 再做守卫,重复提交自然变成空操作 */
|
|
98
104
|
async function commitRename(s) {
|
|
99
105
|
if (editingId.value !== s.id) return
|
|
100
106
|
editingId.value = ''
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
103
|
-
await renameSession(s.id,
|
|
107
|
+
const next = draft.value.trim()
|
|
108
|
+
if (!next || next === (s.title || '')) return
|
|
109
|
+
await renameSession(s.id, next)
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
async function removeSession(s) {
|
|
107
|
-
const name = s.title || '
|
|
108
|
-
|
|
109
|
-
if (!confirm(`删除会话「${name}」?\n对话历史会一起删掉,不可恢复。`)) return
|
|
113
|
+
const name = s.title || t('rail.untitled')
|
|
114
|
+
if (!confirm(t('rail.deleteConfirm', { name }))) return
|
|
110
115
|
await deleteSession(s.id)
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -117,19 +122,19 @@ async function startNew() {
|
|
|
117
122
|
|
|
118
123
|
function fmt(ts) {
|
|
119
124
|
if (!ts) return ''
|
|
120
|
-
return new Date(ts * 1000).toLocaleString(
|
|
125
|
+
return new Date(ts * 1000).toLocaleString(numberLocale(), {
|
|
121
126
|
month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false,
|
|
122
127
|
})
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
function meta(s) {
|
|
126
|
-
return [fmt(s.updated_at), s.message_count ?
|
|
131
|
+
return [fmt(s.updated_at), s.message_count ? t('common.count', { n: s.message_count }) : ''].filter(Boolean).join(' · ')
|
|
127
132
|
}
|
|
128
133
|
</script>
|
|
129
134
|
|
|
130
135
|
<template>
|
|
131
136
|
<aside class="col col--left rail-left">
|
|
132
|
-
<button class="new-sess" title="
|
|
137
|
+
<button class="new-sess" :title="t('rail.newSession')" @click="startNew">{{ t('rail.newSessionBtn') }}</button>
|
|
133
138
|
|
|
134
139
|
<nav class="rail-nav">
|
|
135
140
|
<div
|
|
@@ -142,7 +147,7 @@ function meta(s) {
|
|
|
142
147
|
<span class="ic">
|
|
143
148
|
<svg viewBox="0 0 24 24"><path v-for="(d, i) in icon(m.icon)" :key="i" :d="d" /></svg>
|
|
144
149
|
</span>
|
|
145
|
-
<b>{{ m.
|
|
150
|
+
<b>{{ moduleLabel(m.id) }}</b>
|
|
146
151
|
<span v-if="m.beta" class="beta">BETA</span>
|
|
147
152
|
</button>
|
|
148
153
|
|
|
@@ -151,7 +156,7 @@ function meta(s) {
|
|
|
151
156
|
v-for="q in quickOf(m)"
|
|
152
157
|
:key="q.id"
|
|
153
158
|
class="act"
|
|
154
|
-
:title="
|
|
159
|
+
:title="t('rail.open', { label: q.label })"
|
|
155
160
|
:aria-label="q.label"
|
|
156
161
|
@click.stop="pick(q.id)"
|
|
157
162
|
>
|
|
@@ -165,13 +170,13 @@ function meta(s) {
|
|
|
165
170
|
ref="btnRef"
|
|
166
171
|
class="rail-item rail-anchor"
|
|
167
172
|
:aria-expanded="menuOpen"
|
|
168
|
-
title="
|
|
173
|
+
:title="t('rail.otherFeatures')"
|
|
169
174
|
@click="toggleMenu"
|
|
170
175
|
>
|
|
171
176
|
<span class="ic">
|
|
172
177
|
<svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('more')" :key="i" :d="d" /></svg>
|
|
173
178
|
</span>
|
|
174
|
-
<b
|
|
179
|
+
<b>{{ t('rail.more') }}</b>
|
|
175
180
|
<span v-if="moreCur" class="cur">{{ moreCur }}</span>
|
|
176
181
|
<span class="caret" :class="{ open: menuOpen }">▾</span>
|
|
177
182
|
</button>
|
|
@@ -179,13 +184,13 @@ function meta(s) {
|
|
|
179
184
|
</nav>
|
|
180
185
|
|
|
181
186
|
<div class="sec-head">
|
|
182
|
-
|
|
187
|
+
{{ t('rail.history') }} <em>{{ count }}</em>
|
|
183
188
|
</div>
|
|
184
189
|
|
|
185
190
|
<div class="ns-list scroll">
|
|
186
|
-
<div v-if="state.sessionsState === 'loading'" class="empty"
|
|
187
|
-
<div v-else-if="state.sessionsState === 'error'" class="empty"
|
|
188
|
-
<div v-else-if="!count" class="empty"
|
|
191
|
+
<div v-if="state.sessionsState === 'loading'" class="empty">{{ t('common.loading') }}</div>
|
|
192
|
+
<div v-else-if="state.sessionsState === 'error'" class="empty">{{ t('common.offline') }}</div>
|
|
193
|
+
<div v-else-if="!count" class="empty">{{ t('rail.empty') }}</div>
|
|
189
194
|
<template v-else>
|
|
190
195
|
<div
|
|
191
196
|
v-for="s in state.sessions"
|
|
@@ -199,24 +204,24 @@ function meta(s) {
|
|
|
199
204
|
v-model="draft"
|
|
200
205
|
class="ns-edit"
|
|
201
206
|
maxlength="60"
|
|
202
|
-
aria-label="
|
|
207
|
+
:aria-label="t('rail.sessionTitle')"
|
|
203
208
|
@keydown.enter.prevent="commitRename(s)"
|
|
204
209
|
@keydown.esc.prevent="cancelRename"
|
|
205
210
|
@blur="commitRename(s)"
|
|
206
211
|
/>
|
|
207
212
|
<template v-else>
|
|
208
|
-
<button class="ns-item" :title="s.title || '
|
|
209
|
-
<b>{{ s.title || '
|
|
213
|
+
<button class="ns-item" :title="s.title || t('rail.untitled')" @click="pickSession(s)">
|
|
214
|
+
<b>{{ s.title || t('rail.untitled') }}</b>
|
|
210
215
|
<span>{{ meta(s) }}</span>
|
|
211
216
|
</button>
|
|
212
217
|
<span class="ns-acts">
|
|
213
|
-
<button class="act" title="
|
|
218
|
+
<button class="act" :title="t('rail.rename')" @click.stop="beginRename(s)">
|
|
214
219
|
<svg viewBox="0 0 24 24">
|
|
215
220
|
<path d="M4.4 19.6h4l10.8-10.8-4-4L4.4 15.6z" />
|
|
216
221
|
<path d="M14.4 5.6l3.8 3.8" />
|
|
217
222
|
</svg>
|
|
218
223
|
</button>
|
|
219
|
-
<button class="act del" title="
|
|
224
|
+
<button class="act del" :title="t('rail.delete')" @click.stop="removeSession(s)">
|
|
220
225
|
<svg viewBox="0 0 24 24">
|
|
221
226
|
<path d="M5 7h14" />
|
|
222
227
|
<path d="M9.6 7V4.6h4.8V7" />
|
|
@@ -229,6 +234,22 @@ function meta(s) {
|
|
|
229
234
|
</template>
|
|
230
235
|
</div>
|
|
231
236
|
|
|
237
|
+
<div class="rail-foot">
|
|
238
|
+
<div
|
|
239
|
+
v-for="m in pinnedItems"
|
|
240
|
+
:key="m.id"
|
|
241
|
+
class="rail-row"
|
|
242
|
+
:class="{ on: state.module === m.id }"
|
|
243
|
+
>
|
|
244
|
+
<button class="rail-item" :title="moduleLabel(m.id)" @click="pick(m.id)">
|
|
245
|
+
<span class="ic">
|
|
246
|
+
<svg viewBox="0 0 24 24"><path v-for="(d, i) in icon(m.icon)" :key="i" :d="d" /></svg>
|
|
247
|
+
</span>
|
|
248
|
+
<b>{{ moduleLabel(m.id) }}</b>
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
232
253
|
<MenuPopover
|
|
233
254
|
v-if="menuOpen"
|
|
234
255
|
:anchor="anchor"
|
|
@@ -253,7 +274,6 @@ function meta(s) {
|
|
|
253
274
|
.new-sess:hover { background: rgba(26, 157, 95, 0.17); }
|
|
254
275
|
.new-sess:active { transform: scale(0.985); }
|
|
255
276
|
|
|
256
|
-
/* ── 常驻功能入口 ── */
|
|
257
277
|
.rail-nav { display: flex; flex-direction: column; gap: 2px; flex-shrink: 0; }
|
|
258
278
|
|
|
259
279
|
.rail-row { position: relative; display: flex; align-items: center; }
|
|
@@ -285,12 +305,9 @@ function meta(s) {
|
|
|
285
305
|
.rail-row.on .rail-item .ic { color: var(--green); }
|
|
286
306
|
.rail-row.on .rail-item b { font-weight: 600; }
|
|
287
307
|
|
|
288
|
-
/* 停在子视图(复习 / 记忆中心)时,父项给一层更淡的高亮:既说明「你在这条线上」,
|
|
289
|
-
又不和真正选中的白卡混淆。 */
|
|
290
308
|
.rail-row.sub .rail-item { background: rgba(255, 255, 255, 0.5); }
|
|
291
309
|
.rail-row.sub .rail-item .ic { color: var(--green); }
|
|
292
310
|
|
|
293
|
-
/* 成熟度标签:比正式入口新,或者语义还在变 */
|
|
294
311
|
.beta {
|
|
295
312
|
flex-shrink: 0;
|
|
296
313
|
font-size: 10px; font-weight: 700; letter-spacing: 0.08em;
|
|
@@ -299,7 +316,6 @@ function meta(s) {
|
|
|
299
316
|
background: rgba(199, 116, 0, 0.12);
|
|
300
317
|
}
|
|
301
318
|
|
|
302
|
-
/* 同源数据的另一视图:只在 hover / 聚焦时出现,不占常驻视觉 */
|
|
303
319
|
.rail-acts {
|
|
304
320
|
position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
|
|
305
321
|
display: flex; gap: 2px;
|
|
@@ -342,8 +358,13 @@ function meta(s) {
|
|
|
342
358
|
|
|
343
359
|
.ns-list { display: flex; flex-direction: column; gap: 3px; flex: 1; margin: 0 -4px; padding: 0 4px; }
|
|
344
360
|
|
|
345
|
-
|
|
346
|
-
|
|
361
|
+
.rail-foot {
|
|
362
|
+
flex-shrink: 0;
|
|
363
|
+
margin-top: 2px;
|
|
364
|
+
padding-top: 8px;
|
|
365
|
+
border-top: 1px solid var(--line);
|
|
366
|
+
}
|
|
367
|
+
|
|
347
368
|
.ns-row {
|
|
348
369
|
position: relative; display: flex; align-items: center;
|
|
349
370
|
border-radius: var(--r-sm);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
|
3
|
-
import { moreItems } from '../stores/app.js'
|
|
3
|
+
import { groupLabel, moduleLabel, moreItems } from '../stores/app.js'
|
|
4
|
+
import { t } from '../lib/i18n.js'
|
|
4
5
|
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
anchor: { type: Object, default: null },
|
|
@@ -8,18 +9,16 @@ const props = defineProps({
|
|
|
8
9
|
})
|
|
9
10
|
const emit = defineEmits(['pick', 'close'])
|
|
10
11
|
|
|
11
|
-
/* 只剩常驻入口之外的功能(概览 / 活动 / 使用情况 / 复习 / 记忆中心)。
|
|
12
|
-
常驻项不在这里重复出现 —— 「更多」的语义是「别处放不下的」,不是「全部重列一遍」。 */
|
|
13
12
|
const groups = computed(() => {
|
|
14
13
|
const out = []
|
|
15
14
|
let cur = null
|
|
16
15
|
for (const m of moreItems()) {
|
|
17
|
-
if (!m.
|
|
18
|
-
out.push({
|
|
16
|
+
if (!m.groupKey) {
|
|
17
|
+
out.push({ titleKey: '', items: [m] })
|
|
19
18
|
continue
|
|
20
19
|
}
|
|
21
|
-
if (!cur || cur.
|
|
22
|
-
cur = {
|
|
20
|
+
if (!cur || cur.titleKey !== m.groupKey) {
|
|
21
|
+
cur = { titleKey: m.groupKey, items: [] }
|
|
23
22
|
out.push(cur)
|
|
24
23
|
}
|
|
25
24
|
cur.items.push(m)
|
|
@@ -27,7 +26,6 @@ const groups = computed(() => {
|
|
|
27
26
|
return out
|
|
28
27
|
})
|
|
29
28
|
|
|
30
|
-
/* 入口在栏顶 → 浮层向下展开。left 直接对齐按钮左沿;宽度固定,左栏只有 296px,不会溢出视口右侧。 */
|
|
31
29
|
const style = computed(() => {
|
|
32
30
|
const a = props.anchor
|
|
33
31
|
if (!a) return { left: '16px', top: '120px' }
|
|
@@ -61,10 +59,10 @@ onBeforeUnmount(() => {
|
|
|
61
59
|
<template>
|
|
62
60
|
<Teleport to="body">
|
|
63
61
|
<div class="nav-pop" :style="style">
|
|
64
|
-
<div class="pop-head"
|
|
62
|
+
<div class="pop-head">{{ t('menu.title') }} <span class="x" :title="t('menu.close')" @click="$emit('close')">✕</span></div>
|
|
65
63
|
<div class="pop-body">
|
|
66
64
|
<template v-for="(g, gi) in groups" :key="gi">
|
|
67
|
-
<div v-if="g.
|
|
65
|
+
<div v-if="g.titleKey" class="pop-sec">{{ groupLabel(g.titleKey) }}</div>
|
|
68
66
|
<button
|
|
69
67
|
v-for="m in g.items"
|
|
70
68
|
:key="m.id"
|
|
@@ -72,7 +70,7 @@ onBeforeUnmount(() => {
|
|
|
72
70
|
:class="{ on: m.id === active }"
|
|
73
71
|
@click="$emit('pick', m.id)"
|
|
74
72
|
>
|
|
75
|
-
<span class="lbl">{{ m.
|
|
73
|
+
<span class="lbl">{{ moduleLabel(m.id) }}</span>
|
|
76
74
|
<span class="tag">{{ m.tag }}</span>
|
|
77
75
|
</button>
|
|
78
76
|
</template>
|