This commit is contained in:
Timothy Jaeryang Baek
2026-03-06 20:12:37 -06:00
parent 7806cd5aef
commit 6d9996e599
78 changed files with 1340 additions and 466 deletions
+4 -1
View File
@@ -401,7 +401,10 @@
<div
class="w-full {specialPanel && !$showCallOverlay
? ' '
: 'bg-white dark:shadow-lg dark:bg-gray-850'} z-40 pointer-events-auto {activeTab === 'files' ? '' : 'overflow-y-auto'} scrollbar-hidden"
: 'bg-white dark:shadow-lg dark:bg-gray-850'} z-40 pointer-events-auto {activeTab ===
'files'
? ''
: 'overflow-y-auto'} scrollbar-hidden"
id="controls-container"
>
{#if $showCallOverlay}
+57 -39
View File
@@ -590,7 +590,7 @@
{:else}
<div
bind:this={containerEl}
class="flex flex-col h-full min-h-0 relative"
class="flex flex-col h-full min-h-0 min-w-0 relative"
on:dragover={handleDragOver}
on:dragleave={() => (isDragOver = false)}
on:drop={handleDrop}
@@ -709,43 +709,61 @@
</Tooltip>
{/if}
{#if isTextFile}
{#if isHtml && showRaw}
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click={() => filePreviewRef?.saveCodeFile()}
disabled={saving}
aria-label={$i18n.t('Save')}
>
{#if saving}
<Spinner className="size-3.5" />
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5">
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clip-rule="evenodd" />
</svg>
{/if}
</button>
</Tooltip>
{:else if isHtml}
<!-- HTML preview mode: no edit/save buttons -->
{:else if isCode}
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click={() => filePreviewRef?.saveCodeFile()}
disabled={saving}
aria-label={$i18n.t('Save')}
>
{#if saving}
<Spinner className="size-3.5" />
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5">
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clip-rule="evenodd" />
</svg>
{/if}
</button>
</Tooltip>
{:else if editing}
{#if isHtml && showRaw}
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click={() => filePreviewRef?.saveCodeFile()}
disabled={saving}
aria-label={$i18n.t('Save')}
>
{#if saving}
<Spinner className="size-3.5" />
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3.5"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</Tooltip>
{:else if isHtml}
<!-- HTML preview mode: no edit/save buttons -->
{:else if isCode}
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click={() => filePreviewRef?.saveCodeFile()}
disabled={saving}
aria-label={$i18n.t('Save')}
>
{#if saving}
<Spinner className="size-3.5" />
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3.5"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</Tooltip>
{:else if editing}
<Tooltip content={$i18n.t('Cancel')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
@@ -854,7 +872,7 @@
</FileNavToolbar>
<!-- Content -->
<div class="flex-1 overflow-y-auto min-h-0">
<div class="flex-1 overflow-y-auto min-h-0 min-w-0">
{#if selectedFile !== null}
<FilePreview
bind:this={filePreviewRef}
@@ -28,22 +28,24 @@
const isDark = document.documentElement.classList.contains('dark');
const extensions = [
Prec.highest(keymap.of([
{
key: 'Mod-Enter',
run: () => {
dispatch('run');
return true;
Prec.highest(
keymap.of([
{
key: 'Mod-Enter',
run: () => {
dispatch('run');
return true;
}
},
{
key: 'Escape',
run: () => {
dispatch('cancel');
return true;
}
}
},
{
key: 'Escape',
run: () => {
dispatch('cancel');
return true;
}
}
])),
])
),
basicSetup,
keymap.of([indentWithTab]),
indentUnit.of(' '),
@@ -57,7 +59,10 @@
editorLanguage.of([]),
EditorView.theme({
'&': { fontSize: '0.75rem' },
'.cm-content': { padding: '0.35rem 0', fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace' },
'.cm-content': {
padding: '0.35rem 0',
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace'
},
'.cm-gutters': { display: 'none' },
'.cm-focused': { outline: 'none' },
'.cm-scroller': { overflow: 'auto' }
@@ -281,7 +281,7 @@
<div
class="flex-1 {fileImageUrl !== null || (fileOfficeSlides !== null && fileOfficeSlides.length > 0)
? 'overflow-hidden'
: 'overflow-y-auto'} min-h-0 relative h-full"
: 'overflow-y-auto'} min-h-0 min-w-0 relative h-full"
>
<!-- File preview -->
{#if fileLoading}
@@ -298,21 +298,13 @@
{:else if fileVideoUrl !== null}
<div class="w-full h-full flex items-center justify-center bg-black">
<!-- svelte-ignore a11y-media-has-caption -->
<video
src={fileVideoUrl}
controls
class="max-w-full max-h-full"
>
<video src={fileVideoUrl} controls class="max-w-full max-h-full">
{$i18n.t('Your browser does not support the video tag.')}
</video>
</div>
{:else if fileAudioUrl !== null}
<div class="w-full h-full flex items-center justify-center p-6">
<audio
src={fileAudioUrl}
controls
class="w-full max-w-md"
>
<audio src={fileAudioUrl} controls class="w-full max-w-md">
{$i18n.t('Your browser does not support the audio tag.')}
</audio>
</div>
@@ -326,13 +318,15 @@
{@html fileOfficeHtml}
</div>
{#if excelSheetNames.length > 1}
<div class="flex items-center gap-1 py-1.5 px-3 border-t border-gray-100 dark:border-gray-800 overflow-x-auto">
<div
class="flex items-center gap-1 py-1.5 px-3 border-t border-gray-100 dark:border-gray-800 overflow-x-auto"
>
{#each excelSheetNames as sheet}
<button
class="shrink-0 px-3 py-1 text-xs rounded-md transition-colors
{selectedExcelSheet === sheet
? 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}"
? 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}"
on:click={() => onSheetChange?.(sheet)}
>
{sheet}
@@ -343,7 +337,10 @@
</div>
{:else if fileOfficeSlides !== null && fileOfficeSlides.length > 0}
<div class="flex flex-col h-full">
<div class="w-full flex-1 min-h-0 flex items-center justify-center overflow-hidden" use:initImagePanzoom>
<div
class="w-full flex-1 min-h-0 flex items-center justify-center overflow-hidden"
use:initImagePanzoom
>
<img
src={fileOfficeSlides[currentSlide]}
alt="Slide {currentSlide + 1}"
@@ -352,24 +349,50 @@
/>
</div>
{#if fileOfficeSlides.length > 1}
<div class="flex items-center justify-center gap-3 py-2 px-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-500">
<div
class="flex items-center justify-center gap-3 py-2 px-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-500"
>
<button
class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
disabled={currentSlide === 0}
on:click={() => { resetImageView(); currentSlide = Math.max(0, currentSlide - 1); }}
on:click={() => {
resetImageView();
currentSlide = Math.max(0, currentSlide - 1);
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
clip-rule="evenodd"
/>
</svg>
</button>
<span>{currentSlide + 1} / {fileOfficeSlides.length}</span>
<button
class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
disabled={currentSlide === fileOfficeSlides.length - 1}
on:click={() => { resetImageView(); currentSlide = Math.min(fileOfficeSlides.length - 1, currentSlide + 1); }}
on:click={() => {
resetImageView();
currentSlide = Math.min(fileOfficeSlides.length - 1, currentSlide + 1);
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
@@ -402,8 +425,8 @@
{@html renderedHtml}
</div>
{:else if isCsv && !showRaw && csvRows.length > 0}
<div class="overflow-auto h-full px-3 pb-3">
<table class="csv-table w-full text-xs font-mono border-collapse">
<div class="absolute inset-0 overflow-auto px-3 pb-3">
<table class="csv-table text-xs font-mono border-collapse">
<thead>
<tr>
<th class="csv-row-num">#</th>
@@ -439,11 +462,15 @@
{:else if isJson && !showRaw && jsonError}
<div class="p-3 text-xs">
<div class="text-red-500 mb-2">JSON parse error: {jsonError}</div>
<pre class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed">{fileContent}</pre>
<pre
class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed">{fileContent}</pre>
</div>
{:else if isSvg && !showRaw && fileContent}
<div class="svg-preview w-full h-full flex items-center justify-center overflow-auto p-3">
{@html DOMPurify.sanitize(fileContent, { USE_PROFILES: { svg: true, svgFilters: true }, ADD_TAGS: ['use'] })}
{@html DOMPurify.sanitize(fileContent, {
USE_PROFILES: { svg: true, svgFilters: true },
ADD_TAGS: ['use']
})}
</div>
{:else if isCode && !showRaw}
<div class="h-full">
@@ -631,12 +658,29 @@
max-width: 100%;
height: auto;
}
:global(.office-preview h1) { font-size: 1.5rem; font-weight: 700; margin: 0.75em 0 0.5em; }
:global(.office-preview h2) { font-size: 1.25rem; font-weight: 600; margin: 0.75em 0 0.5em; }
:global(.office-preview h3) { font-size: 1.1rem; font-weight: 600; margin: 0.5em 0 0.25em; }
:global(.office-preview p) { margin: 0.25em 0; }
:global(.office-preview h1) {
font-size: 1.5rem;
font-weight: 700;
margin: 0.75em 0 0.5em;
}
:global(.office-preview h2) {
font-size: 1.25rem;
font-weight: 600;
margin: 0.75em 0 0.5em;
}
:global(.office-preview h3) {
font-size: 1.1rem;
font-weight: 600;
margin: 0.5em 0 0.25em;
}
:global(.office-preview p) {
margin: 0.25em 0;
}
:global(.office-preview ul),
:global(.office-preview ol) { padding-left: 1.5em; margin: 0.5em 0; }
:global(.office-preview ol) {
padding-left: 1.5em;
margin: 0.5em 0;
}
/* ── Shiki code highlighting ─────────────────────────────────── */
.shiki-preview :global(pre.shiki) {
margin: 0;
@@ -679,4 +723,3 @@
text-decoration: var(--shiki-dark-text-decoration) !important;
}
</style>
@@ -39,7 +39,13 @@
<div class="json-children">
{#each entries as [k, v], i}
<div class="json-entry">
<svelte:self data={v} key={type === 'array' ? null : k} root={false} depth={depth + 1} {expandDepth} />
<svelte:self
data={v}
key={type === 'array' ? null : k}
root={false}
depth={depth + 1}
{expandDepth}
/>
{#if i < entries.length - 1}<span class="json-comma">,</span>{/if}
</div>
{/each}
@@ -38,11 +38,12 @@
$: cells = (notebook?.cells as NotebookCell[]) ?? [];
$: lang = (notebook?.metadata as Record<string, unknown>)?.kernelspec
? ((notebook.metadata as Record<string, Record<string, string>>).kernelspec?.language ?? 'python')
? ((notebook.metadata as Record<string, Record<string, string>>).kernelspec?.language ??
'python')
: 'python';
const toStr = (s: string[] | string | undefined): string =>
Array.isArray(s) ? s.join('') : s ?? '';
Array.isArray(s) ? s.join('') : (s ?? '');
// ── Syntax highlighting ──────────────────────────────────────────────
let highlightedCells: Record<number, string> = {};
@@ -75,8 +76,7 @@
const renderMarkdown = (src: string): string =>
DOMPurify.sanitize(marked.parse(src, { async: false }) as string);
const stripAnsi = (s: string): string =>
s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
const stripAnsi = (s: string): string => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
const getOutputImages = (output: NotebookOutput): string[] => {
if (!output.data) return [];
@@ -173,12 +173,14 @@
const result = await executeNotebookCell(baseUrl, apiKey, sessionId!, index, source);
if ('error' in result) {
cells[index].outputs = [{
output_type: 'error',
ename: 'ExecutionError',
evalue: result.error,
traceback: [result.error]
}];
cells[index].outputs = [
{
output_type: 'error',
ename: 'ExecutionError',
evalue: result.error,
traceback: [result.error]
}
];
} else {
cells[index].outputs = result.outputs;
if (result.execution_count !== undefined) {
@@ -225,7 +227,13 @@
{$i18n.t('Run All')}
</button>
{#if kernelReady}
<button class="nb-btn text-[0.6rem]" on:click={async () => { await stopSession(); await startSession(); }}>
<button
class="nb-btn text-[0.6rem]"
on:click={async () => {
await stopSession();
await startSession();
}}
>
{$i18n.t('Restart')}
</button>
<button class="nb-btn text-[0.6rem]" on:click={stopSession}>
@@ -237,19 +245,30 @@
<div class="flex items-center select-none">
{#if kernelStarting}
<Tooltip content={$i18n.t('Starting kernel...')} placement="bottom"><Spinner className="size-2" /></Tooltip>
<Tooltip content={$i18n.t('Starting kernel...')} placement="bottom"
><Spinner className="size-2" /></Tooltip
>
{:else if runningCell !== null}
<Tooltip content={$i18n.t('Running')} placement="bottom"><Spinner className="size-2" /></Tooltip>
<Tooltip content={$i18n.t('Running')} placement="bottom"
><Spinner className="size-2" /></Tooltip
>
{:else if kernelReady}
<Tooltip content="Jupyter" placement="bottom"><span class="size-1.5 rounded-full bg-green-500 inline-block"></span></Tooltip>
<Tooltip content="Jupyter" placement="bottom"
><span class="size-1.5 rounded-full bg-green-500 inline-block"></span></Tooltip
>
{:else}
<Tooltip content={$i18n.t('No kernel')} placement="bottom"><span class="size-1.5 rounded-full bg-gray-400 dark:bg-gray-600 inline-block"></span></Tooltip>
<Tooltip content={$i18n.t('No kernel')} placement="bottom"
><span class="size-1.5 rounded-full bg-gray-400 dark:bg-gray-600 inline-block"
></span></Tooltip
>
{/if}
</div>
</div>
{#if kernelError}
<div class="px-2 py-1 text-[0.65rem] text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 whitespace-pre-wrap font-mono">
<div
class="px-2 py-1 text-[0.65rem] text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 whitespace-pre-wrap font-mono"
>
{kernelError}
</div>
{/if}
@@ -265,7 +284,9 @@
bind:value={editedSources[i]}
on:input={autoResize}
on:blur={() => cancelEditing(i)}
on:keydown={(e) => { if (e.key === 'Escape') cancelEditing(i); }}
on:keydown={(e) => {
if (e.key === 'Escape') cancelEditing(i);
}}
></textarea>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -290,8 +311,19 @@
disabled={runningCell !== null}
title="Run cell (⌘+Enter)"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z"
/>
</svg>
</button>
{/if}
@@ -308,7 +340,9 @@
<CellEditor
value={editedSources[i]}
{lang}
on:change={(e) => { editedSources[i] = e.detail; }}
on:change={(e) => {
editedSources[i] = e.detail;
}}
on:run={() => runCell(i)}
on:cancel={() => cancelEditing(i)}
/>
@@ -334,7 +368,9 @@
<div class="nb-outputs">
{#each cell.outputs as output}
{#if output.output_type === 'error'}
<pre class="nb-error">{stripAnsi((output.traceback ?? []).join('\n') || `${output.ename}: ${output.evalue}`)}</pre>
<pre class="nb-error">{stripAnsi(
(output.traceback ?? []).join('\n') || `${output.ename}: ${output.evalue}`
)}</pre>
{:else}
{@const html = getOutputHtml(output)}
{@const images = getOutputImages(output)}
@@ -401,10 +437,18 @@
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.nb-markdown :global(h1) { font-size: 1.4rem; }
.nb-markdown :global(h2) { font-size: 1.2rem; }
.nb-markdown :global(h3) { font-size: 1.05rem; }
.nb-markdown :global(p) { margin: 0.4em 0; }
.nb-markdown :global(h1) {
font-size: 1.4rem;
}
.nb-markdown :global(h2) {
font-size: 1.2rem;
}
.nb-markdown :global(h3) {
font-size: 1.05rem;
}
.nb-markdown :global(p) {
margin: 0.4em 0;
}
.nb-markdown :global(pre) {
background: rgba(128, 128, 128, 0.06);
padding: 0.5rem 0.75rem;
@@ -495,7 +539,8 @@
background: #161b22;
color: #e6edf3;
}
.nb-code-textarea, .nb-edit-textarea {
.nb-code-textarea,
.nb-edit-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
+27 -29
View File
@@ -67,33 +67,33 @@
</svg>
{$i18n.t('Ports')}
<span class="ml-auto flex items-center gap-1">
{#if ports.length > 0}
<span
class="text-[10px] px-1.5 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
>
{ports.length}
</span>
{/if}
<Tooltip content={$i18n.t('Refresh')}>
<button
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click|stopPropagation={loadPorts}
aria-label={$i18n.t('Refresh')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3 {loading ? 'animate-spin' : ''}"
{#if ports.length > 0}
<span
class="text-[10px] px-1.5 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
{ports.length}
</span>
{/if}
<Tooltip content={$i18n.t('Refresh')}>
<button
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click|stopPropagation={loadPorts}
aria-label={$i18n.t('Refresh')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3 {loading ? 'animate-spin' : ''}"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
</span>
</button>
@@ -112,9 +112,7 @@
<span class="font-mono text-blue-500 dark:text-blue-400 shrink-0">
:{port.port}
</span>
<span
class="text-gray-500 dark:text-gray-400 truncate flex-1 text-left"
>
<span class="text-gray-500 dark:text-gray-400 truncate flex-1 text-left">
{port.process ?? ''}
</span>
<span
@@ -75,9 +75,7 @@
// Get page data
const offset = page * pageSize;
const result = db.exec(
`SELECT * FROM "${selectedTable}" LIMIT ${pageSize} OFFSET ${offset}`
);
const result = db.exec(`SELECT * FROM "${selectedTable}" LIMIT ${pageSize} OFFSET ${offset}`);
if (result.length > 0) {
rows = result[0].values.map((r: any[]) => r.map((v: any) => formatValue(v)));
} else {
@@ -119,7 +117,9 @@
onDestroy(() => {
if (db) {
try { db.close(); } catch {}
try {
db.close();
} catch {}
db = null;
}
});
@@ -132,7 +132,9 @@
<div class="p-3 text-xs text-red-500">{error}</div>
{:else}
<!-- Table tabs + query toggle -->
<div class="flex items-center gap-1 px-2 py-1.5 border-b border-gray-100 dark:border-gray-800 overflow-x-auto scrollbar-none shrink-0">
<div
class="flex items-center gap-1 px-2 py-1.5 border-b border-gray-100 dark:border-gray-800 overflow-x-auto scrollbar-none shrink-0"
>
{#each tables as table}
<button
class="shrink-0 px-2 py-1 text-xs rounded-lg transition
@@ -150,7 +152,9 @@
{queryMode
? 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 font-medium'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}"
on:click={() => { queryMode = !queryMode; }}
on:click={() => {
queryMode = !queryMode;
}}
>
SQL
</button>
@@ -172,9 +176,13 @@
}
}}
></textarea>
<div class="flex items-center justify-between px-2 py-1 bg-gray-50 dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div
class="flex items-center justify-between px-2 py-1 bg-gray-50 dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800"
>
{#if queryError}
<span class="text-[0.65rem] text-red-500 dark:text-red-400 truncate mr-2">{queryError}</span>
<span class="text-[0.65rem] text-red-500 dark:text-red-400 truncate mr-2"
>{queryError}</span
>
{:else}
<span class="text-[0.6rem] text-gray-400 dark:text-gray-600 select-none">⌘+Enter</span>
{/if}
@@ -242,24 +250,50 @@
<!-- Pagination -->
{#if totalPages > 1}
<div class="flex items-center justify-center gap-3 py-1.5 px-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-500 shrink-0">
<div
class="flex items-center justify-center gap-3 py-1.5 px-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-500 shrink-0"
>
<button
class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
disabled={page === 0}
on:click={() => { page--; loadPage(); }}
on:click={() => {
page--;
loadPage();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
clip-rule="evenodd"
/>
</svg>
</button>
<span>{page + 1} / {totalPages} ({totalRows.toLocaleString()} rows)</span>
<button
class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
disabled={page >= totalPages - 1}
on:click={() => { page++; loadPage(); }}
on:click={() => {
page++;
loadPage();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
@@ -205,7 +205,7 @@
? model?.id
: (selectedModels ?? []).length > 0
? selectedModels.at(0)
: model?.id ?? null}
: (model?.id ?? null)}
messages={createMessagesList(history, messageId)}
onAdd={({ modelId, parentId, messages }) => {
console.log(modelId, parentId, messages);
+28 -7
View File
@@ -587,7 +587,9 @@
{#if docxError}
<div class="text-red-500 text-sm p-4">{docxError}</div>
{:else if docxHtml}
<div class="office-preview max-h-[60vh] overflow-auto p-4 prose dark:prose-invert max-w-full text-sm">
<div
class="office-preview max-h-[60vh] overflow-auto p-4 prose dark:prose-invert max-w-full text-sm"
>
{@html docxHtml}
</div>
{:else}
@@ -611,20 +613,39 @@
<button
class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
disabled={pptxCurrentSlide === 0}
on:click={() => pptxCurrentSlide = Math.max(0, pptxCurrentSlide - 1)}
on:click={() => (pptxCurrentSlide = Math.max(0, pptxCurrentSlide - 1))}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
clip-rule="evenodd"
/>
</svg>
</button>
<span>{pptxCurrentSlide + 1} / {pptxSlides.length}</span>
<button
class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
disabled={pptxCurrentSlide === pptxSlides.length - 1}
on:click={() => pptxCurrentSlide = Math.min(pptxSlides.length - 1, pptxCurrentSlide + 1)}
on:click={() =>
(pptxCurrentSlide = Math.min(pptxSlides.length - 1, pptxCurrentSlide + 1))}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>