mulmoclaude 0.3.0 → 0.4.0
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/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
- package/client/assets/index-CubzmCVK.css +2 -0
- package/client/assets/{index-eHWB79u5.js → index-DtcyExH9.js} +80 -61
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
- package/client/index.html +2 -4
- package/package.json +13 -13
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +2 -1
- package/server/agent/prompt.ts +187 -26
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +3 -3
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +13 -12
- package/server/api/routes/html.ts +2 -2
- package/server/api/routes/image.ts +7 -7
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +16 -6
- package/server/api/routes/roles.ts +2 -2
- package/server/api/routes/scheduler.ts +8 -6
- package/server/api/routes/schedulerTasks.ts +5 -3
- package/server/api/routes/sessions.ts +2 -2
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +5 -5
- package/server/api/routes/sources.ts +3 -3
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +22 -8
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +6 -6
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +17 -16
- package/server/index.ts +40 -46
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/files/atomic.ts +11 -11
- package/server/utils/files/image-store.ts +17 -6
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +4 -4
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/spreadsheet-store.ts +5 -5
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +2 -2
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/optimizationPass.ts +2 -2
- package/server/workspace/journal/state.ts +6 -6
- package/server/workspace/paths.ts +3 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +3 -3
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +2 -7
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/sourceState.ts +9 -4
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +286 -112
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +60 -26
- package/src/components/FileContentHeader.vue +7 -4
- package/src/components/FileContentRenderer.vue +20 -6
- package/src/components/FileTree.vue +6 -3
- package/src/components/FileTreePane.vue +11 -8
- package/src/components/FilesView.vue +5 -3
- package/src/components/LockStatusPopup.vue +15 -12
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/SessionHistoryPanel.vue +33 -29
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +43 -30
- package/src/components/SettingsModal.vue +21 -19
- package/src/components/SettingsReferenceDirsTab.vue +29 -24
- package/src/components/SettingsWorkspaceDirsTab.vue +32 -22
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +5 -2
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +17 -12
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +15 -10
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +5 -2
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useDynamicFavicon.ts +172 -37
- package/src/composables/useEventListeners.ts +7 -8
- package/src/composables/useFaviconState.ts +13 -2
- package/src/composables/useFileSelection.ts +24 -6
- package/src/composables/useLayoutMode.ts +32 -0
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useViewLayout.ts +20 -34
- package/src/lang/de.ts +536 -0
- package/src/lang/en.ts +558 -0
- package/src/lang/es.ts +543 -0
- package/src/lang/fr.ts +536 -0
- package/src/lang/ja.ts +536 -0
- package/src/lang/ko.ts +540 -0
- package/src/lang/pt-BR.ts +534 -0
- package/src/lang/zh.ts +537 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +2 -0
- package/src/plugins/canvas/View.vue +102 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +59 -43
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +26 -22
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +24 -34
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +53 -24
- package/src/plugins/scheduler/View.vue +28 -19
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/spreadsheet/Preview.vue +8 -3
- package/src/plugins/spreadsheet/View.vue +21 -12
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +27 -7
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +5 -2
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +42 -24
- package/src/router/index.ts +41 -26
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +19 -0
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/path/workspaceLinkRouter.ts +81 -0
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-Bm70FDU2.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/utils/canvas/viewMode.ts +0 -46
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Pure formatters for scheduler-task display. Extracted from
|
|
2
|
+
// TasksTab.vue so the UTC → local conversion can be unit-tested
|
|
3
|
+
// without spinning up Vue.
|
|
4
|
+
//
|
|
5
|
+
// Internally every task stores its daily trigger as `HH:MM` in UTC
|
|
6
|
+
// (that's what the scheduler engine fires on). The UI should show the
|
|
7
|
+
// same moment in the viewer's local timezone so a user in Tokyo sees
|
|
8
|
+
// "Daily 05:00 JST" instead of "Daily 20:00 UTC".
|
|
9
|
+
|
|
10
|
+
export interface DailySchedule {
|
|
11
|
+
type: "daily";
|
|
12
|
+
time: string; // "HH:MM" in UTC
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IntervalSchedule {
|
|
16
|
+
type: "interval";
|
|
17
|
+
intervalMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TaskSchedule = DailySchedule | IntervalSchedule | { type: string; [k: string]: unknown };
|
|
21
|
+
|
|
22
|
+
const DAILY_TIME_RE = /^(\d{1,2}):(\d{2})$/;
|
|
23
|
+
|
|
24
|
+
// Build a Date anchored to `now`'s local calendar day but at the
|
|
25
|
+
// requested UTC wall-clock hour/minute. Using today's date (rather
|
|
26
|
+
// than epoch 1970) makes the DST/TZ conversion accurate for the
|
|
27
|
+
// viewer's current moment — "20:00 UTC every day" in Europe/London
|
|
28
|
+
// can differ by an hour between summer and winter.
|
|
29
|
+
function buildUtcInstant(utcHour: number, utcMinute: number, now: Date): Date {
|
|
30
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), utcHour, utcMinute));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Intl formatter configured to surface HH:MM + the short timezone
|
|
34
|
+
// name (e.g. "JST", "PDT"). When the browser can't resolve a zone
|
|
35
|
+
// abbreviation it falls back to the offset string ("GMT+9"), which
|
|
36
|
+
// is fine — the point is that the user doesn't have to mentally
|
|
37
|
+
// convert from UTC.
|
|
38
|
+
const LOCAL_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {
|
|
39
|
+
hour: "2-digit",
|
|
40
|
+
minute: "2-digit",
|
|
41
|
+
hour12: false,
|
|
42
|
+
timeZoneName: "short",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function extractHourMinuteTz(date: Date): { hourMinute: string; tzLabel: string } | null {
|
|
46
|
+
try {
|
|
47
|
+
const parts = LOCAL_TIME_FORMATTER.formatToParts(date);
|
|
48
|
+
const hour = parts.find((part) => part.type === "hour")?.value ?? "";
|
|
49
|
+
const minute = parts.find((part) => part.type === "minute")?.value ?? "";
|
|
50
|
+
const tzLabel = parts.find((part) => part.type === "timeZoneName")?.value ?? "";
|
|
51
|
+
if (!hour || !minute) return null;
|
|
52
|
+
// Intl returns "24" for midnight hour under `hour: "2-digit"` on
|
|
53
|
+
// some runtimes — normalize so "Daily 24:00 JST" never appears.
|
|
54
|
+
const normalizedHour = hour === "24" ? "00" : hour;
|
|
55
|
+
return { hourMinute: `${normalizedHour}:${minute}`, tzLabel };
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Convert a UTC "HH:MM" into "Daily HH:MM <tz>" in the viewer's
|
|
62
|
+
// local zone. Returns the original "Daily HH:MM UTC" string if the
|
|
63
|
+
// input is malformed or the Intl machinery is unavailable — callers
|
|
64
|
+
// never see `null`/throw for a scheduler entry.
|
|
65
|
+
export function formatDailyLocal(utcHHMM: string, now: Date = new Date()): string {
|
|
66
|
+
const match = DAILY_TIME_RE.exec(utcHHMM);
|
|
67
|
+
if (!match) return `Daily ${utcHHMM} UTC`;
|
|
68
|
+
const hour = Number(match[1]);
|
|
69
|
+
const minute = Number(match[2]);
|
|
70
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute) || hour > 23 || minute > 59) {
|
|
71
|
+
return `Daily ${utcHHMM} UTC`;
|
|
72
|
+
}
|
|
73
|
+
const extracted = extractHourMinuteTz(buildUtcInstant(hour, minute, now));
|
|
74
|
+
if (!extracted) return `Daily ${utcHHMM} UTC`;
|
|
75
|
+
return `Daily ${extracted.hourMinute} ${extracted.tzLabel}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatInterval(intervalMs: number): string {
|
|
79
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) return "Every ?";
|
|
80
|
+
const mins = Math.round(intervalMs / 60_000);
|
|
81
|
+
if (mins >= 60) return `Every ${Math.round(mins / 60)}h`;
|
|
82
|
+
return `Every ${mins}m`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatSchedule(schedule: TaskSchedule, now: Date = new Date()): string {
|
|
86
|
+
if (schedule.type === "interval" && typeof (schedule as IntervalSchedule).intervalMs === "number") {
|
|
87
|
+
return formatInterval((schedule as IntervalSchedule).intervalMs);
|
|
88
|
+
}
|
|
89
|
+
if (schedule.type === "daily" && typeof (schedule as DailySchedule).time === "string") {
|
|
90
|
+
return formatDailyLocal((schedule as DailySchedule).time, now);
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(schedule);
|
|
93
|
+
}
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="text-center p-4 bg-green-50 dark:bg-green-900 rounded">
|
|
3
|
-
<div class="text-green-600 dark:text-green-300 font-medium"
|
|
3
|
+
<div class="text-green-600 dark:text-green-300 font-medium">{{ t("pluginSpreadsheet.previewLabel") }}</div>
|
|
4
4
|
<div class="text-sm text-gray-800 dark:text-gray-200 mt-1 font-medium truncate">
|
|
5
5
|
{{ displayTitle }}
|
|
6
6
|
</div>
|
|
7
|
-
<div v-if="sheetCount > 1" class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
7
|
+
<div v-if="sheetCount > 1" class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
8
|
+
{{ t("pluginSpreadsheet.previewSheets", sheetCount, { named: { count: sheetCount } }) }}
|
|
9
|
+
</div>
|
|
8
10
|
</div>
|
|
9
11
|
</template>
|
|
10
12
|
|
|
11
13
|
<script setup lang="ts">
|
|
12
14
|
import { computed } from "vue";
|
|
15
|
+
import { useI18n } from "vue-i18n";
|
|
13
16
|
import type { ToolResult } from "gui-chat-protocol";
|
|
14
17
|
import type { SpreadsheetToolData } from "./definition";
|
|
15
18
|
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
|
|
16
21
|
const props = defineProps<{
|
|
17
22
|
result: ToolResult<SpreadsheetToolData>;
|
|
18
23
|
}>();
|
|
19
24
|
|
|
20
25
|
const displayTitle = computed(() => {
|
|
21
|
-
return props.result.title || "
|
|
26
|
+
return props.result.title || t("pluginSpreadsheet.previewUntitled");
|
|
22
27
|
});
|
|
23
28
|
|
|
24
29
|
const sheetCount = computed(() => {
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="spreadsheet-container">
|
|
3
3
|
<div v-if="loading" class="min-h-full p-8 flex items-center justify-center">
|
|
4
|
-
<div class="text-gray-500">
|
|
4
|
+
<div class="text-gray-500">{{ t("pluginSpreadsheet.loading") }}</div>
|
|
5
5
|
</div>
|
|
6
6
|
<div v-else-if="errorMessage" class="min-h-full p-8 flex items-center justify-center">
|
|
7
7
|
<div class="error">{{ errorMessage }}</div>
|
|
8
8
|
</div>
|
|
9
9
|
<div v-else-if="!resolvedSheets || resolvedSheets.length === 0" class="min-h-full p-8 flex items-center justify-center">
|
|
10
|
-
<div class="text-gray-500">
|
|
10
|
+
<div class="text-gray-500">{{ t("pluginSpreadsheet.noData") }}</div>
|
|
11
11
|
</div>
|
|
12
12
|
<template v-else>
|
|
13
13
|
<div class="spreadsheet-content-wrapper">
|
|
14
14
|
<div class="p-4">
|
|
15
15
|
<div class="header">
|
|
16
16
|
<h1 class="title">
|
|
17
|
-
{{ selectedResult.title || "
|
|
17
|
+
{{ selectedResult.title || t("pluginSpreadsheet.untitled") }}
|
|
18
18
|
</h1>
|
|
19
19
|
<div class="button-group">
|
|
20
20
|
<button class="download-btn excel-btn" @click="downloadExcel">
|
|
21
21
|
<span class="material-icons">download</span>
|
|
22
|
-
|
|
22
|
+
{{ t("pluginSpreadsheet.excel") }}
|
|
23
23
|
</button>
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
|
|
44
44
|
<!-- Collapsible Editor -->
|
|
45
45
|
<details v-if="!miniEditorOpen" ref="editorDetails" class="spreadsheet-source">
|
|
46
|
-
<summary>
|
|
46
|
+
<summary>{{ t("pluginSpreadsheet.editData") }}</summary>
|
|
47
47
|
<textarea ref="editorTextarea" v-model="editableData" class="spreadsheet-editor" spellcheck="false" @input="handleDataEdit"></textarea>
|
|
48
|
-
<button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">
|
|
48
|
+
<button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">{{ t("pluginSpreadsheet.applyChanges") }}</button>
|
|
49
49
|
</details>
|
|
50
50
|
|
|
51
51
|
<!-- Mini Editor at Bottom -->
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
<div class="radio-group">
|
|
58
58
|
<label class="radio-option">
|
|
59
59
|
<input v-model="miniEditorType" type="radio" value="string" />
|
|
60
|
-
|
|
60
|
+
{{ t("pluginSpreadsheet.stringType") }}
|
|
61
61
|
</label>
|
|
62
62
|
<label class="radio-option">
|
|
63
63
|
<input v-model="miniEditorType" type="radio" value="object" />
|
|
64
|
-
|
|
64
|
+
{{ t("pluginSpreadsheet.formulaType") }}
|
|
65
65
|
</label>
|
|
66
66
|
</div>
|
|
67
67
|
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
v-model="miniEditorValue"
|
|
72
72
|
type="text"
|
|
73
73
|
class="form-input"
|
|
74
|
-
placeholder="
|
|
74
|
+
:placeholder="t('pluginSpreadsheet.valuePlaceholder')"
|
|
75
75
|
@keyup.enter="saveMiniEditor"
|
|
76
76
|
/>
|
|
77
77
|
|
|
@@ -81,13 +81,19 @@
|
|
|
81
81
|
v-model="miniEditorFormula"
|
|
82
82
|
type="text"
|
|
83
83
|
class="form-input"
|
|
84
|
-
placeholder="
|
|
84
|
+
:placeholder="t('pluginSpreadsheet.valueOrFormulaPlaceholder')"
|
|
85
|
+
@keyup.enter="saveMiniEditor"
|
|
86
|
+
/>
|
|
87
|
+
<input
|
|
88
|
+
v-model="miniEditorFormat"
|
|
89
|
+
type="text"
|
|
90
|
+
class="form-input"
|
|
91
|
+
:placeholder="t('pluginSpreadsheet.formatPlaceholder')"
|
|
85
92
|
@keyup.enter="saveMiniEditor"
|
|
86
93
|
/>
|
|
87
|
-
<input v-model="miniEditorFormat" type="text" class="form-input" placeholder="Format (e.g., $#,##0.00)" @keyup.enter="saveMiniEditor" />
|
|
88
94
|
</template>
|
|
89
95
|
|
|
90
|
-
<button class="save-btn" @click="saveMiniEditor">
|
|
96
|
+
<button class="save-btn" @click="saveMiniEditor">{{ t("pluginSpreadsheet.update") }}</button>
|
|
91
97
|
<button class="cancel-btn" @click="closeMiniEditor">✕</button>
|
|
92
98
|
</div>
|
|
93
99
|
</div>
|
|
@@ -97,6 +103,7 @@
|
|
|
97
103
|
|
|
98
104
|
<script setup lang="ts">
|
|
99
105
|
import { computed, ref, watch, onMounted, onUnmounted } from "vue";
|
|
106
|
+
import { useI18n } from "vue-i18n";
|
|
100
107
|
import * as XLSX from "xlsx";
|
|
101
108
|
import type { ToolResult } from "gui-chat-protocol";
|
|
102
109
|
import type { SpreadsheetToolData, SpreadsheetSheet } from "./definition";
|
|
@@ -119,6 +126,8 @@ import { API_ROUTES } from "../../config/apiRoutes";
|
|
|
119
126
|
import type { FilesContentResponseLike } from "./engine/responseDecoder";
|
|
120
127
|
import { isObj, isRecord } from "../../utils/types";
|
|
121
128
|
|
|
129
|
+
const { t } = useI18n();
|
|
130
|
+
|
|
122
131
|
/**
|
|
123
132
|
* Normalize malformed data structures
|
|
124
133
|
* Some models generate flat arrays instead of 2D arrays - fix them
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-sm leading-snug" :class="textColorClass">
|
|
3
|
-
<div class="preview-markdown" v-html="renderedMarkdown" />
|
|
4
|
-
</div>
|
|
2
|
+
<div class="preview-text p-2 text-sm leading-snug" :class="textColorClass">{{ previewText }}</div>
|
|
5
3
|
</template>
|
|
6
4
|
|
|
7
5
|
<script setup lang="ts">
|
|
@@ -14,7 +12,6 @@ const props = defineProps<{
|
|
|
14
12
|
result: ToolResultComplete<TextResponseData>;
|
|
15
13
|
}>();
|
|
16
14
|
|
|
17
|
-
const previewText = computed(() => props.result.data?.text ?? "");
|
|
18
15
|
const messageRole = computed(() => props.result.data?.role ?? "assistant");
|
|
19
16
|
|
|
20
17
|
const textColorClass = computed(() => {
|
|
@@ -28,67 +25,27 @@ const textColorClass = computed(() => {
|
|
|
28
25
|
}
|
|
29
26
|
});
|
|
30
27
|
|
|
31
|
-
const
|
|
28
|
+
const previewText = computed(() => markdownToPlainText(props.result.data?.text ?? ""));
|
|
29
|
+
|
|
30
|
+
function markdownToPlainText(markdown: string): string {
|
|
31
|
+
const html = marked(markdown, { breaks: true, gfm: true }) as string;
|
|
32
|
+
const spaced = html
|
|
33
|
+
.replace(/<\/(td|th)>/gi, " ")
|
|
34
|
+
.replace(/<\/(p|h[1-6]|li|tr|blockquote|pre|div)>/gi, "$&\n")
|
|
35
|
+
.replace(/<br\s*\/?>/gi, "\n");
|
|
36
|
+
const doc = new DOMParser().parseFromString(spaced, "text/html");
|
|
37
|
+
const text = doc.body.textContent ?? "";
|
|
38
|
+
return text.replace(/\n{3,}/g, "\n\n").trim();
|
|
39
|
+
}
|
|
32
40
|
</script>
|
|
33
41
|
|
|
34
42
|
<style scoped>
|
|
35
|
-
.preview-
|
|
43
|
+
.preview-text {
|
|
36
44
|
overflow: hidden;
|
|
37
45
|
display: -webkit-box;
|
|
38
46
|
-webkit-line-clamp: 5;
|
|
39
47
|
-webkit-box-orient: vertical;
|
|
40
|
-
/* Links inside v-html would otherwise hijack the row-level
|
|
41
|
-
select click and navigate to the article URL. */
|
|
42
|
-
pointer-events: none;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.preview-markdown :deep(p) {
|
|
46
|
-
margin: 0;
|
|
47
|
-
display: inline;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.preview-markdown :deep(h1),
|
|
51
|
-
.preview-markdown :deep(h2),
|
|
52
|
-
.preview-markdown :deep(h3),
|
|
53
|
-
.preview-markdown :deep(h4),
|
|
54
|
-
.preview-markdown :deep(h5),
|
|
55
|
-
.preview-markdown :deep(h6) {
|
|
56
|
-
font-size: inherit;
|
|
57
|
-
font-weight: bold;
|
|
58
|
-
display: inline;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.preview-markdown :deep(ul),
|
|
62
|
-
.preview-markdown :deep(ol) {
|
|
63
|
-
display: inline;
|
|
64
|
-
margin: 0;
|
|
65
|
-
padding: 0;
|
|
66
|
-
list-style: none;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.preview-markdown :deep(li) {
|
|
70
|
-
display: inline;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.preview-markdown :deep(li)::before {
|
|
74
|
-
content: "• ";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.preview-markdown :deep(strong) {
|
|
78
|
-
font-weight: bold;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.preview-markdown :deep(em) {
|
|
82
|
-
font-style: italic;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.preview-markdown :deep(code) {
|
|
86
|
-
font-family: monospace;
|
|
87
|
-
font-size: 0.9em;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.preview-markdown :deep(pre) {
|
|
91
|
-
display: inline;
|
|
92
48
|
white-space: pre-wrap;
|
|
49
|
+
word-break: break-word;
|
|
93
50
|
}
|
|
94
51
|
</style>
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<div class="button-group">
|
|
5
5
|
<button class="download-btn download-btn-green" :disabled="pdfDownloading" @click="downloadPdf">
|
|
6
6
|
<span class="material-icons">{{ pdfDownloading ? "hourglass_empty" : "download" }}</span>
|
|
7
|
-
|
|
7
|
+
{{ t("pluginTextResponse.pdf") }}
|
|
8
8
|
</button>
|
|
9
9
|
</div>
|
|
10
|
-
<span v-if="pdfError" class="text-xs text-red-500 self-center ml-2" :title="pdfError"
|
|
10
|
+
<span v-if="pdfError" class="text-xs text-red-500 self-center ml-2" :title="pdfError">{{ t("pluginTextResponse.pdfFailed") }}</span>
|
|
11
11
|
</div>
|
|
12
12
|
<div class="flex-1 overflow-hidden relative" @click.capture="openLinksInNewTab">
|
|
13
13
|
<div class="text-response-container">
|
|
@@ -27,28 +27,36 @@
|
|
|
27
27
|
|
|
28
28
|
<!-- Collapsible Editor -->
|
|
29
29
|
<details v-if="editable" ref="detailsEl" class="text-response-source" data-testid="text-response-edit">
|
|
30
|
-
<summary data-testid="text-response-edit-summary">
|
|
30
|
+
<summary data-testid="text-response-edit-summary">{{ t("pluginTextResponse.editContent") }}</summary>
|
|
31
31
|
<textarea v-model="editedText" class="text-response-editor" spellcheck="false" data-testid="text-response-edit-textarea"></textarea>
|
|
32
|
-
<button class="apply-btn" :disabled="!hasChanges" data-testid="text-response-apply-btn" @click="applyChanges">
|
|
32
|
+
<button class="apply-btn" :disabled="!hasChanges" data-testid="text-response-apply-btn" @click="applyChanges">
|
|
33
|
+
{{ t("pluginTextResponse.applyChanges") }}
|
|
34
|
+
</button>
|
|
33
35
|
</details>
|
|
34
36
|
</div>
|
|
35
|
-
<button v-show="!editing" class="copy-btn" :title="copied ? '
|
|
37
|
+
<button v-show="!editing" class="copy-btn" :title="copied ? t('pluginTextResponse.copiedLabel') : t('pluginTextResponse.copyLabel')" @click="copyText">
|
|
36
38
|
<span class="material-icons">{{ copied ? "check" : "content_copy" }}</span>
|
|
37
39
|
</button>
|
|
38
|
-
<button v-show="editing" class="cancel-btn" @click="cancelEdit">
|
|
40
|
+
<button v-show="editing" class="cancel-btn" @click="cancelEdit">{{ t("pluginTextResponse.cancel") }}</button>
|
|
39
41
|
</div>
|
|
40
42
|
</div>
|
|
41
43
|
</template>
|
|
42
44
|
|
|
43
45
|
<script setup lang="ts">
|
|
44
46
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
|
47
|
+
import { useI18n } from "vue-i18n";
|
|
45
48
|
import { marked } from "marked";
|
|
46
49
|
import type { ToolResult, ToolResultComplete } from "gui-chat-protocol/vue";
|
|
47
50
|
import type { TextResponseData } from "./types";
|
|
48
51
|
import { handleExternalLinkClick } from "../../utils/dom/externalLink";
|
|
52
|
+
import { classifyWorkspacePath } from "../../utils/path/workspaceLinkRouter";
|
|
53
|
+
import { useAppApi } from "../../composables/useAppApi";
|
|
49
54
|
import { usePdfDownload } from "../../composables/usePdfDownload";
|
|
50
55
|
import { useClipboardCopy } from "../../composables/useClipboardCopy";
|
|
51
56
|
|
|
57
|
+
const { t } = useI18n();
|
|
58
|
+
const appApi = useAppApi();
|
|
59
|
+
|
|
52
60
|
const props = withDefaults(
|
|
53
61
|
defineProps<{
|
|
54
62
|
selectedResult: ToolResultComplete<TextResponseData>;
|
|
@@ -160,7 +168,19 @@ function applyChanges() {
|
|
|
160
168
|
const isAssistant = computed(() => (props.selectedResult.data?.role ?? "assistant") === "assistant");
|
|
161
169
|
|
|
162
170
|
function openLinksInNewTab(event: MouseEvent): void {
|
|
163
|
-
handleExternalLinkClick(event);
|
|
171
|
+
if (handleExternalLinkClick(event)) return;
|
|
172
|
+
// Internal workspace-path links (rendered by marked from agent
|
|
173
|
+
// Markdown): route to the appropriate view instead of letting them
|
|
174
|
+
// navigate the SPA to a non-existent session route.
|
|
175
|
+
const target = event.target as HTMLElement;
|
|
176
|
+
const anchor = target.closest("a");
|
|
177
|
+
if (!anchor) return;
|
|
178
|
+
const href = anchor.getAttribute("href");
|
|
179
|
+
if (!href || href.startsWith("#")) return;
|
|
180
|
+
if (classifyWorkspacePath(href)) {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
appApi.navigateToWorkspacePath(href);
|
|
183
|
+
}
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
const { pdfDownloading, pdfError, downloadPdf: rawDownloadPdf } = usePdfDownload();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-sm">
|
|
2
|
+
<div class="p-2 text-sm">
|
|
3
3
|
<div class="flex items-center gap-1 font-medium text-gray-700 mb-1">
|
|
4
|
-
<span
|
|
5
|
-
<span>{{
|
|
4
|
+
<span aria-hidden="true">{{ t("todoPreview.headerIcon") }}</span>
|
|
5
|
+
<span>{{ t("todoPreview.completedRatio", { done: completedCount, total: items.length }) }}</span>
|
|
6
6
|
</div>
|
|
7
7
|
<div
|
|
8
8
|
v-for="item in preview"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
class="text-xs truncate flex items-center gap-1"
|
|
11
11
|
:class="item.completed ? 'line-through text-gray-400' : 'text-gray-600'"
|
|
12
12
|
>
|
|
13
|
-
<span class="shrink-0">{{ item.completed ? "
|
|
13
|
+
<span class="shrink-0">{{ item.completed ? t("todoPreview.doneIcon") : t("todoPreview.pendingIcon") }}</span>
|
|
14
14
|
<span class="truncate">{{ item.text }}</span>
|
|
15
15
|
<template v-if="(item.labels?.length ?? 0) > 0">
|
|
16
16
|
<span
|
|
@@ -20,16 +20,21 @@
|
|
|
20
20
|
:class="colorForLabel(label)"
|
|
21
21
|
>{{ label }}</span
|
|
22
22
|
>
|
|
23
|
-
<span v-if="(item.labels?.length ?? 0) > 2" class="text-[9px] text-gray-400 shrink-0"
|
|
23
|
+
<span v-if="(item.labels?.length ?? 0) > 2" class="text-[9px] text-gray-400 shrink-0">{{
|
|
24
|
+
t("todoPreview.moreLabels", { count: (item.labels?.length ?? 0) - 2 })
|
|
25
|
+
}}</span>
|
|
24
26
|
</template>
|
|
25
27
|
</div>
|
|
26
|
-
<div v-if="more > 0" class="text-xs text-gray-400"
|
|
28
|
+
<div v-if="more > 0" class="text-xs text-gray-400">{{ t("todoPreview.moreItems", { count: more }) }}</div>
|
|
27
29
|
</div>
|
|
28
30
|
</template>
|
|
29
31
|
|
|
30
32
|
<script setup lang="ts">
|
|
31
33
|
import { computed, ref, watch } from "vue";
|
|
34
|
+
import { useI18n } from "vue-i18n";
|
|
32
35
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
36
|
+
|
|
37
|
+
const { t } = useI18n();
|
|
33
38
|
import type { TodoData, TodoItem } from "./index";
|
|
34
39
|
import { useFreshPluginData } from "../../composables/useFreshPluginData";
|
|
35
40
|
import { API_ROUTES } from "../../config/apiRoutes";
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
<!-- API error banner — surfaces POST /api/todos failures so a
|
|
4
4
|
silent add/remove/toggle becomes diagnosable. -->
|
|
5
5
|
<div v-if="todoApiError" class="px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700" role="alert" data-testid="todo-api-error">
|
|
6
|
-
|
|
6
|
+
{{ t("pluginTodo.apiError", { error: todoApiError }) }}
|
|
7
7
|
</div>
|
|
8
8
|
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
|
|
9
|
-
<h2 class="text-lg font-semibold text-gray-800">
|
|
10
|
-
<span class="text-sm text-gray-500">{{
|
|
9
|
+
<h2 class="text-lg font-semibold text-gray-800">{{ t("pluginTodo.heading") }}</h2>
|
|
10
|
+
<span class="text-sm text-gray-500">{{ t("pluginTodo.completedRatio", { done: completedCount, total: items.length }) }}</span>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
<!-- Filter bar: only shown when at least one label is in use. -->
|
|
14
14
|
<div v-if="labelInventory.length > 0" class="flex flex-wrap items-center gap-1.5 px-6 py-2 border-b border-gray-100 bg-gray-50">
|
|
15
|
-
<span class="text-xs text-gray-500 mr-1">
|
|
15
|
+
<span class="text-xs text-gray-500 mr-1">{{ t("pluginTodo.filter") }}</span>
|
|
16
16
|
<button
|
|
17
17
|
v-for="entry in labelInventory"
|
|
18
18
|
:key="entry.label"
|
|
@@ -27,14 +27,21 @@
|
|
|
27
27
|
{{ entry.label }}
|
|
28
28
|
<span class="opacity-60">{{ entry.count }}</span>
|
|
29
29
|
</button>
|
|
30
|
-
<button
|
|
31
|
-
|
|
30
|
+
<button
|
|
31
|
+
v-if="activeFilters.size > 0"
|
|
32
|
+
class="ml-auto text-xs text-gray-500 hover:text-gray-700"
|
|
33
|
+
:title="t('pluginTodo.clearFilters')"
|
|
34
|
+
@click="clearFilters"
|
|
35
|
+
>
|
|
36
|
+
{{ t("pluginTodo.clearButton") }}
|
|
32
37
|
</button>
|
|
33
38
|
</div>
|
|
34
39
|
|
|
35
|
-
<div v-if="items.length === 0" class="flex-1 flex items-center justify-center text-gray-400">
|
|
40
|
+
<div v-if="items.length === 0" class="flex-1 flex items-center justify-center text-gray-400">{{ t("pluginTodo.noItems") }}</div>
|
|
36
41
|
|
|
37
|
-
<div v-else-if="filteredItems.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
|
42
|
+
<div v-else-if="filteredItems.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
|
43
|
+
{{ t("pluginTodo.noMatchingFilter") }}
|
|
44
|
+
</div>
|
|
38
45
|
|
|
39
46
|
<ul v-else class="flex-1 overflow-y-auto p-4 space-y-2">
|
|
40
47
|
<li v-for="item in filteredItems" :key="item.id" class="rounded-lg border" :class="selectedId === item.id ? 'border-blue-400' : 'border-gray-200'">
|
|
@@ -62,10 +69,10 @@
|
|
|
62
69
|
</div>
|
|
63
70
|
<button
|
|
64
71
|
class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0"
|
|
65
|
-
title="
|
|
72
|
+
:title="t('pluginTodo.deleteItem')"
|
|
66
73
|
@click.stop="remove(item)"
|
|
67
74
|
>
|
|
68
|
-
|
|
75
|
+
{{ t("pluginTodo.deleteSymbol") }}
|
|
69
76
|
</button>
|
|
70
77
|
<span class="material-icons text-gray-400 text-sm" :title="selectedId === item.id ? 'Collapse' : 'Expand'">
|
|
71
78
|
{{ selectedId === item.id ? "expand_less" : "expand_more" }}
|
|
@@ -80,20 +87,25 @@
|
|
|
80
87
|
spellcheck="false"
|
|
81
88
|
/>
|
|
82
89
|
<div class="flex items-center gap-2">
|
|
83
|
-
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">
|
|
84
|
-
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">
|
|
90
|
+
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">{{ t("pluginTodo.update") }}</button>
|
|
91
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">
|
|
92
|
+
{{ t("common.cancel") }}
|
|
93
|
+
</button>
|
|
85
94
|
<span v-if="yamlError" class="text-xs text-red-500">{{ yamlError }}</span>
|
|
86
95
|
</div>
|
|
87
96
|
</div>
|
|
88
97
|
</li>
|
|
89
98
|
</ul>
|
|
90
99
|
|
|
91
|
-
<button v-if="hasCompleted" class="mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start" @click="clearCompleted">
|
|
100
|
+
<button v-if="hasCompleted" class="mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start" @click="clearCompleted">
|
|
101
|
+
{{ t("pluginTodo.clearCompleted") }}
|
|
102
|
+
</button>
|
|
92
103
|
</div>
|
|
93
104
|
</template>
|
|
94
105
|
|
|
95
106
|
<script setup lang="ts">
|
|
96
107
|
import { computed, ref, watch } from "vue";
|
|
108
|
+
import { useI18n } from "vue-i18n";
|
|
97
109
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
98
110
|
import type { TodoData, TodoItem } from "./index";
|
|
99
111
|
import { useFreshPluginData } from "../../composables/useFreshPluginData";
|
|
@@ -101,6 +113,8 @@ import { apiPost } from "../../utils/api";
|
|
|
101
113
|
import { API_ROUTES } from "../../config/apiRoutes";
|
|
102
114
|
import { colorForLabel, filterByLabels, listLabelsWithCount, subtractLabels } from "./labels";
|
|
103
115
|
|
|
116
|
+
const { t } = useI18n();
|
|
117
|
+
|
|
104
118
|
const props = defineProps<{
|
|
105
119
|
selectedResult: ToolResultComplete<TodoData>;
|
|
106
120
|
}>();
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="min-h-24 flex items-center justify-center">
|
|
3
3
|
<img v-if="resolvedSrc" :src="resolvedSrc" class="max-w-full h-auto rounded" :alt="alt" />
|
|
4
|
-
<div v-else class="text-gray-400 text-sm">
|
|
4
|
+
<div v-else class="text-gray-400 text-sm">{{ t("common.noImageYet") }}</div>
|
|
5
5
|
</div>
|
|
6
6
|
</template>
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
9
|
import { computed } from "vue";
|
|
10
|
+
import { useI18n } from "vue-i18n";
|
|
10
11
|
import type { ToolResult } from "gui-chat-protocol/vue";
|
|
11
12
|
import type { ImageToolData } from "./types";
|
|
12
|
-
import {
|
|
13
|
+
import { resolveImageSrcFresh } from "../../utils/image/resolve";
|
|
14
|
+
|
|
15
|
+
const { t } = useI18n();
|
|
13
16
|
|
|
14
17
|
const props = withDefaults(
|
|
15
18
|
defineProps<{
|
|
@@ -19,5 +22,5 @@ const props = withDefaults(
|
|
|
19
22
|
{ alt: "Image" },
|
|
20
23
|
);
|
|
21
24
|
|
|
22
|
-
const resolvedSrc = computed(() => (props.result.data?.imageData ?
|
|
25
|
+
const resolvedSrc = computed(() => (props.result.data?.imageData ? resolveImageSrcFresh(props.result.data.imageData) : ""));
|
|
23
26
|
</script>
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<div v-if="resolvedSrc" class="flex-1 flex items-center justify-center min-h-0">
|
|
5
5
|
<img :src="resolvedSrc" class="max-w-full max-h-full object-contain rounded" :alt="alt" />
|
|
6
6
|
</div>
|
|
7
|
-
<div v-else class="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
|
7
|
+
<div v-else class="flex-1 flex items-center justify-center text-gray-400 text-sm">{{ t("common.noImageYet") }}</div>
|
|
8
8
|
<div v-if="selectedResult.data?.prompt" class="mt-4 p-3 bg-gray-100 rounded-lg max-w-full flex-shrink-0">
|
|
9
9
|
<p class="text-sm text-gray-700">
|
|
10
|
-
<span class="font-medium">{{ promptLabel }}
|
|
10
|
+
<span class="font-medium">{{ t("pluginUiImage.promptLabel", { label: promptLabel }) }}</span>
|
|
11
11
|
{{ selectedResult.data.prompt }}
|
|
12
12
|
</p>
|
|
13
13
|
</div>
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
<script setup lang="ts">
|
|
19
19
|
import { computed } from "vue";
|
|
20
|
+
import { useI18n } from "vue-i18n";
|
|
20
21
|
import type { ToolResult } from "gui-chat-protocol/vue";
|
|
21
22
|
import type { ImageToolData } from "./types";
|
|
22
|
-
import {
|
|
23
|
+
import { resolveImageSrcFresh } from "../../utils/image/resolve";
|
|
24
|
+
|
|
25
|
+
const { t } = useI18n();
|
|
23
26
|
|
|
24
27
|
const props = withDefaults(
|
|
25
28
|
defineProps<{
|
|
@@ -30,5 +33,5 @@ const props = withDefaults(
|
|
|
30
33
|
{ alt: "Image", promptLabel: "Prompt" },
|
|
31
34
|
);
|
|
32
35
|
|
|
33
|
-
const resolvedSrc = computed(() => (props.selectedResult.data?.imageData ?
|
|
36
|
+
const resolvedSrc = computed(() => (props.selectedResult.data?.imageData ? resolveImageSrcFresh(props.selectedResult.data.imageData) : ""));
|
|
34
37
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-sm">
|
|
2
|
+
<div class="p-2 text-sm">
|
|
3
3
|
<div class="flex items-center gap-1 font-medium text-gray-700 mb-1">
|
|
4
4
|
<span class="material-icons" style="font-size: 14px">menu_book</span>
|
|
5
5
|
<span>{{ label }}</span>
|
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
<div v-for="entry in previewEntries" :key="entry.slug" class="text-xs text-gray-500 truncate">
|
|
8
8
|
{{ entry.title }}
|
|
9
9
|
</div>
|
|
10
|
-
<div v-if="more > 0" class="text-xs text-gray-400"
|
|
10
|
+
<div v-if="more > 0" class="text-xs text-gray-400">{{ t("pluginWiki.previewMore", { count: more }) }}</div>
|
|
11
11
|
</div>
|
|
12
12
|
</template>
|
|
13
13
|
|
|
14
14
|
<script setup lang="ts">
|
|
15
15
|
import { computed, ref, watch } from "vue";
|
|
16
|
+
import { useI18n } from "vue-i18n";
|
|
16
17
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n();
|
|
17
20
|
import type { WikiData, WikiPageEntry } from "./index";
|
|
18
21
|
import { useFreshPluginData } from "../../composables/useFreshPluginData";
|
|
19
22
|
import { API_ROUTES } from "../../config/apiRoutes";
|