diff --git a/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml b/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml index e4ae6c255b..99cbe8a373 100644 --- a/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml +++ b/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml @@ -66,7 +66,7 @@ services: ipc: host restart: always codegen-xeon-ui-server: - image: ${REGISTRY:-opea}/codegen-gradio-ui:${TAG:-latest} + image: ${REGISTRY:-opea}/codegen-ui:${TAG:-latest} container_name: codegen-xeon-ui-server depends_on: - codegen-xeon-backend-server diff --git a/CodeGen/docker_compose/intel/cpu/xeon/compose_tgi.yaml b/CodeGen/docker_compose/intel/cpu/xeon/compose_tgi.yaml index 7c1c3802e5..0da9cdddd3 100644 --- a/CodeGen/docker_compose/intel/cpu/xeon/compose_tgi.yaml +++ b/CodeGen/docker_compose/intel/cpu/xeon/compose_tgi.yaml @@ -66,7 +66,7 @@ services: ipc: host restart: always codegen-xeon-ui-server: - image: ${REGISTRY:-opea}/codegen-gradio-ui:${TAG:-latest} + image: ${REGISTRY:-opea}/codegen-ui:${TAG:-latest} container_name: codegen-xeon-ui-server depends_on: - codegen-xeon-backend-server diff --git a/CodeGen/tests/test_compose_on_xeon.sh b/CodeGen/tests/test_compose_on_xeon.sh index 5be0455d74..ba75e95aad 100644 --- a/CodeGen/tests/test_compose_on_xeon.sh +++ b/CodeGen/tests/test_compose_on_xeon.sh @@ -27,7 +27,7 @@ function build_docker_images() { popd && sleep 1s echo "Build all the images with --no-cache, check docker_image_build.log for details..." - service_list="codegen codegen-gradio-ui llm-textgen dataprep retriever embedding" + service_list="codegen codegen-ui llm-textgen dataprep retriever embedding" docker compose -f build.yaml build ${service_list} --no-cache > ${LOG_PATH}/docker_image_build.log @@ -174,6 +174,8 @@ function validate_frontend() { npm install && npm ci && npx playwright install --with-deps node -v && npm -v && pip list + export no_proxy="localhost,127.0.0.1,$ip_address" + exit_status=0 npx playwright test || exit_status=$? @@ -244,8 +246,12 @@ function main() { validate_megaservice echo "::endgroup::" - echo "::group::validate_gradio" - validate_gradio + # echo "::group::validate_gradio" + # validate_gradio + # echo "::endgroup::" + + echo "::group::validate_ui" + validate_frontend echo "::endgroup::" stop_docker "${docker_compose_files[${i}]}" diff --git a/CodeGen/ui/svelte/src/lib/modules/chat/Output.svelte b/CodeGen/ui/svelte/src/lib/modules/chat/Output.svelte index 015375ad6f..5bf3012431 100644 --- a/CodeGen/ui/svelte/src/lib/modules/chat/Output.svelte +++ b/CodeGen/ui/svelte/src/lib/modules/chat/Output.svelte @@ -37,12 +37,24 @@ import bash from "svelte-highlight/languages/bash"; import sql from "svelte-highlight/languages/sql"; import { marked } from "marked"; - export let label = ""; + import { afterUpdate, onMount } from "svelte"; + export let output = ""; - export let languages = "Python"; + export let lang = "Python"; export let isCode = false; + export let md_output = ""; + export let segments: Segment[] = []; + let outputEl: HTMLDivElement; let copyText = "copy"; + let shouldAutoscroll = true; + + type Segment = { + id: number; + type: "text" | "code"; + content: string; + lang?: string; + }; const languagesTag = { Typescript: typescript, @@ -65,53 +77,194 @@ Lua: lua, Bash: bash, Sql: sql, - } as { [key: string]: any }; - - function copyToClipboard(text) { - const textArea = document.createElement("textarea"); - textArea.value = text; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand("copy"); - document.body.removeChild(textArea); + } as const; + + type LangKey = keyof typeof languagesTag; + + const aliasMap: Record = { + javascript: "Javascript", + js: "Javascript", + jsx: "Javascript", + typescript: "Typescript", + ts: "Typescript", + tsx: "Typescript", + + python: "Python", + py: "Python", + + c: "C", + "c++": "Cpp", + cpp: "Cpp", + cxx: "Cpp", + csharp: "Csharp", + "c#": "Csharp", + + go: "Go", + golang: "Go", + java: "Java", + swift: "Swift", + ruby: "Ruby", + rust: "Rust", + php: "Php", + kotlin: "Kotlin", + objectivec: "Objectivec", + objc: "Objectivec", + "objective-c": "Objectivec", + perl: "Perl", + matlab: "Matlab", + r: "R", + lua: "Lua", + + bash: "Bash", + sh: "Bash", + shell: "Bash", + zsh: "Bash", + + sql: "Sql", + }; + + $: normalizedLangKey = (() => { + const raw = (lang ?? "").toString().trim(); + if (!raw) return null; + const lower = raw.toLowerCase(); + + if (lower in aliasMap) return aliasMap[lower]; + + const hit = (Object.keys(languagesTag) as LangKey[]).find( + (k) => k.toLowerCase() === lower + ); + return hit ?? null; + })(); + + $: fullText = buildFullText(); + + function atBottom(el: HTMLElement, threshold = 8) { + return el.scrollHeight - el.scrollTop - el.clientHeight <= threshold; + } + + function handleScroll() { + if (!outputEl) return; + shouldAutoscroll = atBottom(outputEl); } - function handelCopy() { - copyToClipboard(output); + function scrollToBottom() { + if (!outputEl) return; + requestAnimationFrame(() => + requestAnimationFrame(() => { + if (outputEl.scrollHeight) { + outputEl.scrollTop = outputEl.scrollHeight; + } + }) + ); + } + + onMount(() => { + scrollToBottom(); + }); + + afterUpdate(() => { + if (shouldAutoscroll) scrollToBottom(); + }); + async function copyAllFromDiv() { + await navigator.clipboard.writeText(outputEl.innerText); copyText = "copied!"; - setTimeout(() => { - copyText = "copy"; - }, 1000); + setTimeout(() => (copyText = "copy"), 1000); + } + + function copyToClipboard(text: string) { + if (navigator?.clipboard?.writeText) { + navigator.clipboard.writeText(text); + } else { + const textArea = document.createElement("textarea"); + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand("copy"); + document.body.removeChild(textArea); + } + } + + function normalizeToKey(raw?: string | null) { + const s = (raw ?? "").trim().toLowerCase(); + if (!s) return null; + if (s in aliasMap) return aliasMap[s as keyof typeof aliasMap]; + const hit = ( + Object.keys(languagesTag) as (keyof typeof languagesTag)[] + ).find((k) => k.toLowerCase() === s); + return hit ?? null; + } + + function buildFullText(): string { + if (segments && segments.length > 0) { + return segments + .map((seg) => { + if (seg.type === "code") { + const key = normalizeToKey(seg.lang) ?? "text"; + return ["```" + key.toLowerCase(), seg.content, "```"].join("\n"); + } + return seg.content; + }) + .join("\n\n"); + } + + const parts: string[] = []; + if (isCode && output) { + const key = (normalizedLangKey ?? "text").toLowerCase(); + parts.push(["```" + key, output, "```"].join("\n")); + } + if (md_output) { + parts.push(md_output); + } + return parts.join("\n\n"); }
- {label} -
{copyText}
+
- {#if isCode} - - - + {#if segments && segments.length > 0} + {#each segments as seg (seg.id)} + {#if seg.type === "code"} +
+ + + +
+ {:else} +
{@html marked(seg.content)}
+ {/if} + {/each} {:else} -
- {@html marked(output)} -
+ {#if isCode && output} + + + + {/if} + {#if md_output} +
+ {@html marked(md_output)} +
+ {/if} {/if}
@@ -120,17 +273,8 @@ .hiddenScroll::-webkit-scrollbar { display: none; } - .hiddenScroll { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } - - .code-format-style { - resize: none; - font-size: 16px; - border: solid rgba(128, 0, 128, 0) 4px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.19); - transition: 0.1s linear; - } diff --git a/CodeGen/ui/svelte/src/routes/+page.svelte b/CodeGen/ui/svelte/src/routes/+page.svelte index 0e7d43beaf..d927ea9322 100644 --- a/CodeGen/ui/svelte/src/routes/+page.svelte +++ b/CodeGen/ui/svelte/src/routes/+page.svelte @@ -23,38 +23,196 @@ import PaperAirplane from "$lib/assets/chat/svelte/PaperAirplane.svelte"; import Output from "$lib/modules/chat/Output.svelte"; - let code_output: string = ""; let query: string = ""; let loading: boolean = false; - let deleteFlag: boolean = false; + let inFence = false; + let tickRun = 0; + let skipLangLine = false; + let langBuf = ""; + let currentLang = ""; + + type Segment = { + id: number; + type: "text" | "code"; + content: string; + lang?: string; + }; + let segments: Segment[] = []; + let _sid = 0; + + const languageAliases: Record = { + javascript: "Javascript", + js: "Javascript", + jsx: "Javascript", + typescript: "Typescript", + ts: "Typescript", + tsx: "Typescript", + + python: "Python", + py: "Python", + + c: "C", + "c++": "Cpp", + cpp: "Cpp", + cxx: "Cpp", + csharp: "Csharp", + "c#": "Csharp", + + go: "Go", + golang: "Go", + java: "Java", + swift: "Swift", + ruby: "Ruby", + rust: "Rust", + php: "Php", + kotlin: "Kotlin", + objectivec: "Objectivec", + objc: "Objectivec", + "objective-c": "Objectivec", + perl: "Perl", + matlab: "Matlab", + r: "R", + lua: "Lua", + + bash: "Bash", + sh: "Bash", + shell: "Bash", + zsh: "Bash", + + sql: "Sql", + }; + + function canonicalLang(raw?: string | null): string | null { + const s = (raw ?? "").toString().trim(); + if (!s) return null; + const lower = s.toLowerCase(); + return languageAliases[lower] ?? s; + } + + function appendText(s: string) { + if (!s) return; + const last = segments[segments.length - 1]; + if (!last || last.type !== "text") { + segments = [...segments, { id: ++_sid, type: "text", content: "" }]; + } + segments[segments.length - 1].content += s; + } + + function appendCode(s: string) { + if (!s) return; + const last = segments[segments.length - 1]; + if (!last || last.type !== "code") { + segments = [ + ...segments, + { + id: ++_sid, + type: "code", + content: "", + lang: currentLang || "python", + }, + ]; + } + segments[segments.length - 1].content += s; + } + + function settleTicks() { + if (tickRun === 0) return; + + if (tickRun >= 3) { + const toggles = Math.floor(tickRun / 3); + for (let i = 0; i < toggles; i++) { + inFence = !inFence; + if (inFence) { + skipLangLine = true; + langBuf = ""; + currentLang = ""; + } else { + skipLangLine = false; + } + } + const leftovers = tickRun % 3; + if (leftovers) (inFence ? appendCode : appendText)("`".repeat(leftovers)); + } else { + (inFence ? appendCode : appendText)("`".repeat(tickRun)); + } + tickRun = 0; + } + + function consumeChunk(s: string) { + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + + if (ch === "`") { + tickRun++; + continue; + } + + settleTicks(); + + if (skipLangLine) { + if (ch === "\n") { + skipLangLine = false; + const canon = canonicalLang(langBuf); + currentLang = canon ?? (langBuf.trim() || "python"); + langBuf = ""; + } else { + langBuf += ch; + } + continue; + } + + if (inFence) appendCode(ch); + else appendText(ch); + } + } const callTextStream = async (query: string) => { loading = true; - code_output = ""; + + segments = []; + _sid = 0; + inFence = false; + tickRun = 0; + skipLangLine = false; + langBuf = ""; + currentLang = ""; + const eventSource = await fetchTextStream(query); eventSource.addEventListener("message", (e: any) => { - let res = e.data; + const raw = String(e.data); + const payloads = raw + .split(/\r?\n/) + .map((l) => l.replace(/^data:\s*/, "").trim()) + .filter((l) => l.length > 0); - if (res === "[DONE]") { - deleteFlag = false; - loading = false; - query = ''; - } else { - let Msg = JSON.parse(res).choices[0].text; - if (Msg.includes("'''")) { - deleteFlag = true; - } else if (deleteFlag && Msg.includes("\\n")) { - deleteFlag = false; - } else if (Msg !== "" && !deleteFlag) { - code_output += Msg.replace(/\\n/g, "\n"); - } + for (const part of payloads) { + if (part === "[DONE]") { + settleTicks(); + loading = false; + return; } + try { + const json = JSON.parse(part); + const msg = + json.choices?.[0]?.delta?.content ?? json.choices?.[0]?.text ?? ""; + if (!msg || msg === "") continue; + consumeChunk(msg); + } catch (err) { + console.error("JSON chunk parse error:", err, part); + } + } + }); + + eventSource.addEventListener("error", () => { + loading = false; }); + eventSource.stream(); }; const handleTextSubmit = async () => { + if (!query) return; await callTextStream(query); }; @@ -62,48 +220,47 @@
-
-
-
- +
+
+ { - if (event.key === "Enter" && !event.shiftKey && query) { - event.preventDefault(); - handleTextSubmit(); - } - }} - /> - -
+ type="text" + data-testid="code-input" + placeholder="Enter prompt here" + disabled={loading} + maxlength="1200" + bind:value={query} + on:keydown={(event) => { + if (event.key === "Enter" && !event.shiftKey && query) { + event.preventDefault(); + handleTextSubmit(); + } + }} + /> + +
- {#if code_output !== ""} -
- -
+ {#if segments.length} +
+ +
{/if} {#if loading}