mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-15 23:32:40 +00:00
refac
This commit is contained in:
parent
ba34bee2d1
commit
254e29b9af
3 changed files with 334 additions and 11 deletions
|
|
@ -2,6 +2,8 @@
|
|||
import hljs from 'highlight.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { config, pyodideWorker as pyodideWorkerStore } from '$lib/stores';
|
||||
|
||||
import { createPyodideWorker } from '$lib/pyodide/createPyodideWorker';
|
||||
|
|
@ -18,6 +20,7 @@
|
|||
import equal from 'fast-deep-equal';
|
||||
|
||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||
import DiffBlock from './DiffBlock.svelte';
|
||||
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
|
||||
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
|
|
@ -26,7 +29,7 @@
|
|||
import Cube from '$lib/components/icons/Cube.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
export let id = '';
|
||||
export let edit = true;
|
||||
|
|
@ -52,13 +55,9 @@
|
|||
let localPyodideWorker = null;
|
||||
|
||||
let _code = '';
|
||||
$: if (code) {
|
||||
updateCode();
|
||||
}
|
||||
|
||||
const updateCode = () => {
|
||||
_code = code;
|
||||
};
|
||||
$: _code = code;
|
||||
$: isDiff = ['diff', 'patch'].includes(lang.trim().toLowerCase());
|
||||
let editingDiff = false;
|
||||
|
||||
let _token = null;
|
||||
|
||||
|
|
@ -469,6 +468,18 @@
|
|||
</div>
|
||||
|
||||
<div class="flex items-center gap-0.5 shrink-0">
|
||||
{#if isDiff && edit}
|
||||
<button
|
||||
class="bg-none border-none transition rounded-md px-1.5 py-0.5 bg-white dark:bg-black"
|
||||
aria-pressed={editingDiff}
|
||||
on:click={() => {
|
||||
editingDiff = !editingDiff;
|
||||
collapsed = false;
|
||||
}}
|
||||
>
|
||||
{editingDiff ? $i18n.t('Done') : $i18n.t('Edit')}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="flex gap-1 items-center bg-none border-none transition rounded-md px-1.5 py-0.5 bg-white dark:bg-black"
|
||||
on:click={collapseCodeBlock}
|
||||
|
|
@ -542,9 +553,11 @@
|
|||
<div class=" pt-6.5 bg-white dark:bg-black"></div>
|
||||
|
||||
{#if !collapsed}
|
||||
{#if edit}
|
||||
{#if isDiff && !(edit && editingDiff)}
|
||||
<DiffBlock code={_code} />
|
||||
{:else if edit}
|
||||
<CodeEditor
|
||||
value={code}
|
||||
value={isDiff ? _code : code}
|
||||
{id}
|
||||
{lang}
|
||||
onSave={() => {
|
||||
|
|
@ -575,7 +588,7 @@
|
|||
>
|
||||
<span class="text-gray-500 italic">
|
||||
{$i18n.t('{{COUNT}} hidden lines', {
|
||||
COUNT: code.split('\n').length
|
||||
COUNT: (isDiff ? _code : code).split('\n').length
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
197
src/lib/components/chat/Messages/DiffBlock.svelte
Normal file
197
src/lib/components/chat/Messages/DiffBlock.svelte
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<script lang="ts">
|
||||
import { parseDiffRows } from './diff';
|
||||
|
||||
export let code = '';
|
||||
$: rows = parseDiffRows(code);
|
||||
$: showNumbers = rows.some((row) => row.oldNumber !== null || row.newNumber !== null);
|
||||
$: gutterWidth = `${rows.reduce((width, row) => Math.max(width, String(Math.max(row.oldNumber ?? 0, row.newNumber ?? 0)).length + 1), 3)}ch`;
|
||||
</script>
|
||||
|
||||
<div class="diff-block text-sm" dir="ltr" style:--diff-gutter-width={gutterWidth}>
|
||||
<pre><code
|
||||
>{#each rows as row, index}<span class="diff-line {row.type}" class:numbered={showNumbers}
|
||||
>{#if showNumbers}<span
|
||||
class="line-number old-number"
|
||||
data-line-number={row.oldNumber ?? ''}
|
||||
aria-hidden="true"></span><span
|
||||
class="line-number new-number"
|
||||
data-line-number={row.newNumber ?? ''}
|
||||
aria-hidden="true"></span>{/if}<span class="line-prefix">{row.prefix}</span><span
|
||||
class="line-content"
|
||||
>{#each row.segments as segment}<span class:changed={segment.changed}
|
||||
>{segment.text}</span
|
||||
>{/each}{index < rows.length - 1 ? '\n' : ''}</span
|
||||
></span
|
||||
>{/each}</code
|
||||
></pre>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.diff-block {
|
||||
overflow-x: auto;
|
||||
background: #ffffff;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0.5rem 0;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
line-height: 1.6;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-weight: 400;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
display: grid;
|
||||
grid-template-columns: 1.75rem minmax(0, 1fr);
|
||||
min-height: 1.6em;
|
||||
}
|
||||
|
||||
.diff-line.numbered {
|
||||
grid-template-columns: var(--diff-gutter-width) var(--diff-gutter-width) 1.75rem minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.line-number {
|
||||
padding: 0 0.5ch;
|
||||
text-align: right;
|
||||
color: #6e7781;
|
||||
border-inline-end: 1px solid #80808020;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-number::before {
|
||||
content: attr(data-line-number);
|
||||
}
|
||||
|
||||
.line-prefix {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
padding: 0 1rem 0 0.25rem;
|
||||
}
|
||||
|
||||
.addition {
|
||||
background: #dafbe1;
|
||||
color: #116329;
|
||||
box-shadow: inset 3px 0 0 #2da44e;
|
||||
}
|
||||
|
||||
.deletion {
|
||||
background: #ffebe9;
|
||||
color: #a40e26;
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#cf222e 0,
|
||||
#cf222e 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
background-position: left top;
|
||||
background-repeat: repeat-y;
|
||||
background-size: 3px 3px;
|
||||
}
|
||||
|
||||
.hunk {
|
||||
background: #ddf4ff;
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
.file {
|
||||
background: #f6f8fa;
|
||||
color: #57606a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #6e7781;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:global(.dark) .diff-block {
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
:global(.dark) .addition {
|
||||
background: #12261e;
|
||||
color: #aff5b4;
|
||||
box-shadow: inset 3px 0 0 #3fb950;
|
||||
}
|
||||
|
||||
:global(.dark) .deletion {
|
||||
background: #2d161b;
|
||||
color: #ffdcd7;
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#f85149 0,
|
||||
#f85149 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
background-position: left top;
|
||||
background-repeat: repeat-y;
|
||||
background-size: 3px 3px;
|
||||
}
|
||||
|
||||
:global(.dark) .hunk {
|
||||
background: #121f33;
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
:global(.dark) .file {
|
||||
background: #161b22;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
:global(.dark) .meta {
|
||||
color: #8b949e;
|
||||
}
|
||||
.changed {
|
||||
border-radius: 2px;
|
||||
font-weight: 700;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.addition .changed {
|
||||
background: #16a34a40;
|
||||
box-shadow: inset 0 -2px 0 #15803d80;
|
||||
}
|
||||
|
||||
.deletion .changed {
|
||||
background: #dc262630;
|
||||
box-shadow: inset 0 -2px 0 #b91c1c60;
|
||||
}
|
||||
|
||||
:global(.dark) .addition .changed {
|
||||
background: #22c55e50;
|
||||
box-shadow: inset 0 -2px 0 #86efac80;
|
||||
}
|
||||
|
||||
:global(.dark) .deletion .changed {
|
||||
background: #f8717150;
|
||||
box-shadow: inset 0 -2px 0 #fca5a580;
|
||||
}
|
||||
|
||||
:global(.dark) .line-number {
|
||||
color: #8b949e;
|
||||
}
|
||||
</style>
|
||||
113
src/lib/components/chat/Messages/diff.ts
Normal file
113
src/lib/components/chat/Messages/diff.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
export type DiffSegment = { text: string; changed: boolean };
|
||||
export type DiffRow = {
|
||||
type: 'file' | 'hunk' | 'addition' | 'deletion' | 'meta' | 'context';
|
||||
prefix: string;
|
||||
content: string;
|
||||
oldNumber: number | null;
|
||||
newNumber: number | null;
|
||||
segments: DiffSegment[];
|
||||
};
|
||||
|
||||
export function parseDiffRows(code: string): DiffRow[] {
|
||||
let oldNumber = 0;
|
||||
let newNumber = 0;
|
||||
let oldRemaining = 0;
|
||||
let newRemaining = 0;
|
||||
const rows = code.split('\n').map((text): DiffRow => {
|
||||
const row: DiffRow = {
|
||||
type: 'context',
|
||||
prefix: '',
|
||||
content: text,
|
||||
oldNumber: null,
|
||||
newNumber: null,
|
||||
segments: []
|
||||
};
|
||||
const hunk = text.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
||||
const inHunk = oldRemaining > 0 || newRemaining > 0;
|
||||
if (hunk) {
|
||||
row.type = 'hunk';
|
||||
oldNumber = Number(hunk[1]);
|
||||
newNumber = Number(hunk[3]);
|
||||
oldRemaining = Number(hunk[2] ?? 1);
|
||||
newRemaining = Number(hunk[4] ?? 1);
|
||||
} else if (
|
||||
text.startsWith('diff ') ||
|
||||
(!inHunk &&
|
||||
/^(index |---(?:\s|$)|\+\+\+(?:\s|$)|(?:old|new|deleted file|new file) mode |(?:dis)?similarity index |(?:rename|copy) (?:from|to) )/.test(
|
||||
text
|
||||
))
|
||||
) {
|
||||
row.type = 'file';
|
||||
oldRemaining = newRemaining = 0;
|
||||
} else if (text.startsWith('@@')) {
|
||||
// Incomplete streaming headers must not inherit the preceding hunk's numbers.
|
||||
row.type = 'hunk';
|
||||
oldRemaining = newRemaining = 0;
|
||||
} else if (text.startsWith('\\')) {
|
||||
row.type = 'meta';
|
||||
} else {
|
||||
if (text.startsWith('+')) row.type = 'addition';
|
||||
if (text.startsWith('-')) row.type = 'deletion';
|
||||
if (/^[+\- ]/.test(text)) {
|
||||
row.prefix = text[0];
|
||||
row.content = text.slice(1);
|
||||
}
|
||||
if (inHunk && (row.prefix || text === '')) {
|
||||
if (row.type !== 'addition' && oldRemaining > 0) {
|
||||
row.oldNumber = oldNumber++;
|
||||
oldRemaining--;
|
||||
}
|
||||
if (row.type !== 'deletion' && newRemaining > 0) {
|
||||
row.newNumber = newNumber++;
|
||||
newRemaining--;
|
||||
}
|
||||
}
|
||||
}
|
||||
row.segments = [{ text: row.content, changed: false }];
|
||||
return row;
|
||||
});
|
||||
|
||||
for (let i = 0; i < rows.length; ) {
|
||||
const removed: DiffRow[] = [];
|
||||
const added: DiffRow[] = [];
|
||||
while (rows[i]?.type === 'deletion' || rows[i]?.type === 'addition') {
|
||||
(rows[i].type === 'deletion' ? removed : added).push(rows[i++]);
|
||||
}
|
||||
if (removed.length === added.length) {
|
||||
removed.forEach((oldRow, index) => emphasizePair(oldRow, added[index]));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function emphasizePair(oldRow: DiffRow, newRow: DiffRow) {
|
||||
if (
|
||||
oldRow.content === newRow.content ||
|
||||
Math.max(oldRow.content.length, newRow.content.length) >= 1024
|
||||
)
|
||||
return;
|
||||
const oldText = Array.from(oldRow.content);
|
||||
const newText = Array.from(newRow.content);
|
||||
let start = 0;
|
||||
while (start < Math.min(oldText.length, newText.length) && oldText[start] === newText[start])
|
||||
start++;
|
||||
let oldEnd = oldText.length;
|
||||
let newEnd = newText.length;
|
||||
while (oldEnd > start && newEnd > start && oldText[oldEnd - 1] === newText[newEnd - 1]) {
|
||||
oldEnd--;
|
||||
newEnd--;
|
||||
}
|
||||
// ponytail: like computer's Git view, emphasize one changed middle per paired line.
|
||||
// Use a token diff if multiple independent edits need separate spans.
|
||||
for (const [row, text, end] of [
|
||||
[oldRow, oldText, oldEnd],
|
||||
[newRow, newText, newEnd]
|
||||
] as const) {
|
||||
row.segments = [
|
||||
{ text: text.slice(0, start).join(''), changed: false },
|
||||
{ text: text.slice(start, end).join(''), changed: true },
|
||||
{ text: text.slice(end).join(''), changed: false }
|
||||
].filter((segment) => segment.text.length > 0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue