浏览代码

Merge pull request #5955 from EtiennePerot/code-execution-message

feat: add code execution status to chat messages.
Timothy Jaeryang Baek 6 月之前
父节点
当前提交
adb1bfcaa8

+ 23 - 3
src/lib/components/chat/Chat.svelte

@@ -175,10 +175,30 @@
 					message.statusHistory = [data];
 				}
 			} else if (type === 'citation') {
-				if (message?.citations) {
-					message.citations.push(data);
+				if (data?.type === 'code_execution') {
+					// Code execution; update existing code execution by UUID,
+					// otherwise append.
+					if (!message?.code_executions) {
+						message.code_executions = [];
+					}
+					let is_update = false;
+					for (let i = 0; i < message.code_executions.length; i++) {
+						if (message.code_executions[i].uuid === data.uuid) {
+							message.code_executions[i] = data;
+							is_update = true;
+							break;
+						}
+					}
+					if (!is_update) {
+						message.code_executions.push(data);
+					}
 				} else {
-					message.citations = [data];
+					// Regular citation.
+					if (message?.citations) {
+						message.citations.push(data);
+					} else {
+						message.citations = [data];
+					}
 				}
 			} else if (type === 'message') {
 				message.content += data.content;

+ 2 - 1
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -23,6 +23,7 @@
 	export let token;
 	export let lang = '';
 	export let code = '';
+	export let allow_execution = true;
 
 	let _code = '';
 	$: if (code) {
@@ -319,7 +320,7 @@ __builtins__.input = input`);
 					{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
 						{#if executing}
 							<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
-						{:else}
+						{:else if allow_execution}
 							<button
 								class="run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
 								on:click={async () => {

+ 115 - 0
src/lib/components/chat/Messages/CodeExecutionModal.svelte

@@ -0,0 +1,115 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	import CodeBlock from './CodeBlock.svelte';
+	import Modal from '$lib/components/common/Modal.svelte';
+	const i18n = getContext('i18n');
+
+	export let show = false;
+	export let code_execution = null;
+</script>
+
+<Modal size="lg" bind:show>
+	<div>
+		<div class="flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
+			<div class="text-lg font-medium self-center capitalize">
+				{#if code_execution?.status == 'OK'}
+					&#x2705; <!-- Checkmark -->
+				{:else if code_execution?.status == 'ERROR'}
+					&#x274C; <!-- X mark -->
+				{:else if code_execution?.status == 'PENDING'}
+					&#x23F3; <!-- Hourglass -->
+				{:else}
+					&#x2049;&#xFE0F; <!-- Interrobang -->
+				{/if}
+				{#if code_execution?.name}
+					{$i18n.t('Code execution')}: {code_execution?.name}
+				{:else}
+					{$i18n.t('Code execution')}
+				{/if}
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+					code_execution = null;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
+			<div
+				class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden"
+			>
+				<div class="flex flex-col w-full">
+					<div class="text-sm font-medium dark:text-gray-300">
+						{$i18n.t('Code')}
+					</div>
+
+					<CodeBlock
+						id="codeexec-{code_execution?.uuid}-code"
+						lang={code_execution?.language}
+						code={code_execution?.code}
+						allow_execution={false}
+					/>
+				</div>
+				{#if code_execution?.error}
+					<div class="flex flex-col w-full">
+						<hr class=" dark:border-gray-850 my-3" />
+						<div class="text-sm dark:text-gray-400">
+							{$i18n.t('Error')}
+						</div>
+
+						<CodeBlock
+							id="codeexec-{code_execution?.uuid}-error"
+							lang=""
+							code={code_execution?.error}
+							allow_execution={false}
+						/>
+					</div>
+				{/if}
+				{#if code_execution?.output}
+					<div class="flex flex-col w-full">
+						<hr class=" dark:border-gray-850 my-3" />
+						<div class="text-sm dark:text-gray-400">
+							{$i18n.t('Output')}
+						</div>
+
+						<CodeBlock
+							id="codeexec-{code_execution?.uuid}-output"
+							lang=""
+							code={code_execution?.output}
+							allow_execution={false}
+						/>
+					</div>
+				{/if}
+				{#if code_execution?.files && code_execution?.files.length > 0}
+					<div class="flex flex-col w-full">
+						<hr class=" dark:border-gray-850 my-3" />
+						<div class=" text-sm font-medium dark:text-gray-300">
+							{$i18n.t('Files')}
+						</div>
+						<ul class="mt-1 list-disc pl-4 text-xs">
+							{#each code_execution?.files as file}
+								<li>
+									&#x1F4BE; <!-- Floppy disk -->
+									<a href={file.url} target="_blank">{file.name}</a>
+								</li>
+							{/each}
+						</ul>
+					</div>
+				{/if}
+			</div>
+		</div>
+	</div>
+</Modal>

+ 95 - 0
src/lib/components/chat/Messages/CodeExecutions.svelte

@@ -0,0 +1,95 @@
+<script lang="ts">
+	import CodeExecutionModal from './CodeExecutionModal.svelte';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+
+	export let code_executions = [];
+
+	let _code_executions = [];
+
+	$: _code_executions = code_executions.reduce((acc, code_execution) => {
+		let error = null;
+		let output = null;
+		let files = [];
+		let status = 'PENDING';
+		if (code_execution.result) {
+			output = code_execution.result.output;
+			if (code_execution.result.error) {
+				status = 'ERROR';
+				error = code_execution.result.error;
+			} else {
+				status = 'OK';
+			}
+			if (code_execution.result.files) {
+				files = code_execution.result.files;
+			}
+		}
+		acc.push({
+			uuid: code_execution.uuid,
+			name: code_execution.name,
+			code: code_execution.code,
+			language: code_execution.language || '',
+			status: status,
+			error: error,
+			output: output,
+			files: files
+		});
+		return acc;
+	}, []);
+
+	let selectedCodeExecution = null;
+	let showCodeExecutionModal = false;
+</script>
+
+<CodeExecutionModal bind:show={showCodeExecutionModal} code_execution={selectedCodeExecution} />
+
+{#if _code_executions.length > 0}
+	<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
+		{#each _code_executions as code_execution}
+			<div class="flex gap-1 text-xs font-semibold">
+				<button
+					class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
+					on:click={() => {
+						selectedCodeExecution = code_execution;
+						showCodeExecutionModal = true;
+					}}
+				>
+					<div class="bg-white dark:bg-gray-700 rounded-full size-4">
+						{#if code_execution.status == 'OK'}
+							&#x2705; <!-- Checkmark -->
+						{:else if code_execution.status == 'ERROR'}
+							&#x274C; <!-- X mark -->
+						{:else if code_execution.status == 'PENDING'}
+							<Spinner className="size-4" />
+						{:else}
+							&#x2049;&#xFE0F; <!-- Interrobang -->
+						{/if}
+					</div>
+					<div
+						class="flex-1 mx-2 line-clamp-1 code-execution-name {code_execution.status == 'PENDING'
+							? 'pulse'
+							: ''}"
+					>
+						{code_execution.name}
+					</div>
+				</button>
+			</div>
+		{/each}
+	</div>
+{/if}
+
+<style>
+	@keyframes pulse {
+		0%,
+		100% {
+			opacity: 1;
+		}
+		50% {
+			opacity: 0.6;
+		}
+	}
+
+	.pulse {
+		opacity: 1;
+		animation: pulse 1.5s ease;
+	}
+</style>

+ 15 - 0
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -35,6 +35,7 @@
 	import Markdown from './Markdown.svelte';
 	import Error from './Error.svelte';
 	import Citations from './Citations.svelte';
+	import CodeExecutions from './CodeExecutions.svelte';
 
 	import type { Writable } from 'svelte/store';
 	import type { i18n as i18nType } from 'i18next';
@@ -64,6 +65,17 @@
 		done: boolean;
 		error?: boolean | { content: string };
 		citations?: string[];
+		code_executions?: {
+			uuid: string;
+			name: string;
+			code: string;
+			language?: string;
+			result?: {
+				error?: string;
+				output?: string;
+				files?: { name: string; url: string }[];
+			};
+		}[];
 		info?: {
 			openai?: boolean;
 			prompt_tokens?: number;
@@ -516,6 +528,9 @@
 								{#if message.citations}
 									<Citations citations={message.citations} />
 								{/if}
+								{#if message.code_executions}
+									<CodeExecutions code_executions={message.code_executions} />
+								{/if}
 							</div>
 						{/if}
 					</div>