refac
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user