diff --git a/EdgeCraftRAG/Dockerfile.server b/EdgeCraftRAG/Dockerfile.server old mode 100644 new mode 100755 index 5d0aee5cd1..ea1f88786f --- a/EdgeCraftRAG/Dockerfile.server +++ b/EdgeCraftRAG/Dockerfile.server @@ -46,5 +46,4 @@ WORKDIR /home/user/ RUN git clone https://github.com/openvinotoolkit/openvino.genai.git genai ENV PYTHONPATH="$PYTHONPATH:/home/user/genai/tools/llm_bench" - ENTRYPOINT ["python", "-m", "edgecraftrag.server"] \ No newline at end of file diff --git a/EdgeCraftRAG/chatqna.py b/EdgeCraftRAG/chatqna.py old mode 100644 new mode 100755 diff --git a/EdgeCraftRAG/docker_image_build/build.yaml b/EdgeCraftRAG/docker_image_build/build.yaml index 9c80955d9e..c4f2189deb 100644 --- a/EdgeCraftRAG/docker_image_build/build.yaml +++ b/EdgeCraftRAG/docker_image_build/build.yaml @@ -4,7 +4,7 @@ services: edgecraftrag-server: build: - context: ../ + context: .. args: http_proxy: ${http_proxy} https_proxy: ${https_proxy} @@ -12,7 +12,7 @@ services: image: ${REGISTRY:-opea}/edgecraftrag-server:${TAG:-latest} edgecraftrag-ui: build: - context: ../ + context: .. args: http_proxy: ${http_proxy} https_proxy: ${https_proxy} @@ -20,7 +20,7 @@ services: image: ${REGISTRY:-opea}/edgecraftrag-ui:${TAG:-latest} edgecraftrag-ui-gradio: build: - context: ../ + context: .. args: http_proxy: ${http_proxy} https_proxy: ${https_proxy} @@ -28,7 +28,7 @@ services: image: ${REGISTRY:-opea}/edgecraftrag-ui-gradio:${TAG:-latest} edgecraftrag: build: - context: ../ + context: .. args: http_proxy: ${http_proxy} https_proxy: ${https_proxy} diff --git a/EdgeCraftRAG/edgecraftrag/VERSION b/EdgeCraftRAG/edgecraftrag/VERSION new file mode 100644 index 0000000000..7e5dbfbb38 --- /dev/null +++ b/EdgeCraftRAG/edgecraftrag/VERSION @@ -0,0 +1 @@ +25.05-dev-0421 diff --git a/EdgeCraftRAG/edgecraftrag/api/v1/chatqna.py b/EdgeCraftRAG/edgecraftrag/api/v1/chatqna.py old mode 100644 new mode 100755 index d0236c82e8..cc67d2c8cc --- a/EdgeCraftRAG/edgecraftrag/api/v1/chatqna.py +++ b/EdgeCraftRAG/edgecraftrag/api/v1/chatqna.py @@ -5,6 +5,7 @@ from comps.cores.proto.api_protocol import ChatCompletionRequest from edgecraftrag.api_schema import RagOut from edgecraftrag.context import ctx +from edgecraftrag.utils import serialize_contexts from fastapi import FastAPI, File, HTTPException, UploadFile, status from fastapi.responses import StreamingResponse @@ -29,14 +30,15 @@ async def retrieval(request: ChatCompletionRequest): @chatqna_app.post(path="/v1/chatqna") async def chatqna(request: ChatCompletionRequest): try: + request.messages = convert_message(request.messages) generator = ctx.get_pipeline_mgr().get_active_pipeline().generator if generator: request.model = generator.model_id if request.stream: - ret, retri_res = ctx.get_pipeline_mgr().run_pipeline(chat_request=request) + ret, contexts = ctx.get_pipeline_mgr().run_pipeline(chat_request=request) return ret else: - ret, retri_res = ctx.get_pipeline_mgr().run_pipeline(chat_request=request) + ret, contexts = ctx.get_pipeline_mgr().run_pipeline(chat_request=request) return str(ret) except Exception as e: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) @@ -46,7 +48,8 @@ async def chatqna(request: ChatCompletionRequest): @chatqna_app.post(path="/v1/ragqna") async def ragqna(request: ChatCompletionRequest): try: - res, retri_res = ctx.get_pipeline_mgr().run_pipeline(chat_request=request) + request.messages = convert_message(request.messages) + res, contexts = ctx.get_pipeline_mgr().run_pipeline(chat_request=request) if isinstance(res, GeneratedDoc): res = res.text elif isinstance(res, StreamingResponse): @@ -55,10 +58,9 @@ async def ragqna(request: ChatCompletionRequest): collected_data.append(chunk) res = "".join(collected_data) - ragout = RagOut(query=request.messages, contexts=[], response=str(res)) - for n in retri_res: - origin_text = n.node.get_text() - ragout.contexts.append(origin_text.strip()) + serialized_contexts = serialize_contexts(contexts) + + ragout = RagOut(query=request.messages, contexts=serialized_contexts, response=str(res)) return ragout except Exception as e: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) @@ -88,3 +90,34 @@ async def reset_prompt(): return "Reset LLM Prompt Successfully" except Exception as e: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) + + +def convert_message(messages, history_prompt: str = None): + messages_list = [] + if isinstance(messages, str): + str_message = messages + else: + str_message = "" + user_indexs = [i for i, msg in enumerate(messages) if msg.get("role") == "user"] + last_user_index = user_indexs[-1] if user_indexs else -1 + + for idx, message in enumerate(messages): + msg_role = message["role"] + if msg_role in ["user", "assistant"]: + content = message["content"] + if idx == last_user_index and msg_role == "user": + messages_list.append(("system", f"{history_prompt}")) + if isinstance(content, str): + messages_list.append((msg_role, content)) + else: + raise ValueError( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Only text content is supported." + ) + else: + raise ValueError(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Unknown role: {msg_role}") + + for role, content in messages_list: + str_message += f"{role}: {content}\n" + if len(str_message) > 8192: + str_message = str_message[-8192:] + return str_message diff --git a/EdgeCraftRAG/edgecraftrag/api_schema.py b/EdgeCraftRAG/edgecraftrag/api_schema.py index 7a8a493b1e..48c085488e 100644 --- a/EdgeCraftRAG/edgecraftrag/api_schema.py +++ b/EdgeCraftRAG/edgecraftrag/api_schema.py @@ -1,7 +1,7 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Optional +from typing import Any, Optional from pydantic import BaseModel @@ -65,5 +65,5 @@ class FilesIn(BaseModel): class RagOut(BaseModel): query: str - contexts: Optional[list[str]] = None + contexts: Optional[dict[str, Any]] = None response: str diff --git a/EdgeCraftRAG/edgecraftrag/components/data.py b/EdgeCraftRAG/edgecraftrag/components/data.py old mode 100644 new mode 100755 index e7fa19e7ad..34b568abde --- a/EdgeCraftRAG/edgecraftrag/components/data.py +++ b/EdgeCraftRAG/edgecraftrag/components/data.py @@ -54,7 +54,7 @@ def convert_text_to_documents(text) -> List[Document]: def convert_file_to_documents(file_path) -> List[Document]: from llama_index.core import SimpleDirectoryReader - supported_exts = [".pdf", ".txt", ".doc", ".docx", ".pptx", ".ppt", ".csv", ".md", ".html", ".rst"] + supported_exts = [".pdf", ".txt", ".doc", ".docx", ".pptx", ".ppt", ".csv", ".md", ".html", ".rst", ".epub"] if file_path.is_dir(): docs = SimpleDirectoryReader(input_dir=file_path, recursive=True, required_exts=supported_exts).load_data() elif file_path.is_file(): diff --git a/EdgeCraftRAG/edgecraftrag/components/pipeline.py b/EdgeCraftRAG/edgecraftrag/components/pipeline.py index e0a01eba96..a7dd8c4cce 100644 --- a/EdgeCraftRAG/edgecraftrag/components/pipeline.py +++ b/EdgeCraftRAG/edgecraftrag/components/pipeline.py @@ -221,11 +221,13 @@ async def timing_wrapper(): def run_test_generator_ben(pl: Pipeline, chat_request: ChatCompletionRequest) -> Any: benchmark_index, benchmark_data = pl.benchmark.init_benchmark_data() + contexts = {} start = time.perf_counter() query = chat_request.messages retri_res = pl.retriever.run(query=query) query_bundle = QueryBundle(query) benchmark_data[CompType.RETRIEVER] = time.perf_counter() - start + contexts[CompType.RETRIEVER] = retri_res start = time.perf_counter() if pl.postprocessor: @@ -236,6 +238,7 @@ def run_test_generator_ben(pl: Pipeline, chat_request: ChatCompletionRequest) -> ): processor.top_n = chat_request.top_n retri_res = processor.run(retri_res=retri_res, query_bundle=query_bundle) + contexts[CompType.POSTPROCESSOR] = retri_res benchmark_data[CompType.POSTPROCESSOR] = time.perf_counter() - start if pl.generator is None: @@ -260,12 +263,14 @@ def run_test_generator_ben(pl: Pipeline, chat_request: ChatCompletionRequest) -> benchmark_data[CompType.GENERATOR] = end - start pl.benchmark.insert_llm_data(benchmark_index, input_token_size) pl.benchmark.insert_benchmark_data(benchmark_data) - return ret, retri_res + return ret, contexts def run_test_generator(pl: Pipeline, chat_request: ChatCompletionRequest) -> Any: query = chat_request.messages + contexts = {} retri_res = pl.retriever.run(query=query) + contexts[CompType.RETRIEVER] = retri_res query_bundle = QueryBundle(query) if pl.postprocessor: @@ -276,6 +281,7 @@ def run_test_generator(pl: Pipeline, chat_request: ChatCompletionRequest) -> Any ): processor.top_n = chat_request.top_n retri_res = processor.run(retri_res=retri_res, query_bundle=query_bundle) + contexts[CompType.POSTPROCESSOR] = retri_res if pl.generator is None: raise ValueError("No Generator Specified") @@ -286,4 +292,4 @@ def run_test_generator(pl: Pipeline, chat_request: ChatCompletionRequest) -> Any ret = pl.generator.run_vllm(chat_request, retri_res, np_type) else: raise ValueError("LLM inference_type not supported") - return ret, retri_res + return ret, contexts diff --git a/EdgeCraftRAG/edgecraftrag/requirements.txt b/EdgeCraftRAG/edgecraftrag/requirements.txt old mode 100644 new mode 100755 diff --git a/EdgeCraftRAG/edgecraftrag/utils.py b/EdgeCraftRAG/edgecraftrag/utils.py index be83f47135..a185ea0423 100644 --- a/EdgeCraftRAG/edgecraftrag/utils.py +++ b/EdgeCraftRAG/edgecraftrag/utils.py @@ -29,3 +29,14 @@ def iter_elements(cls, paragraph: Paragraph, opts: DocxPartitionerOptions) -> It image.save(image_path) element_metadata = ElementMetadata(image_path=image_path) yield Image(text="IMAGE", metadata=element_metadata) + + +def serialize_node_with_score(node_with_score): + return { + "node": node_with_score.node.__dict__, + "score": node_with_score.score.item() if hasattr(node_with_score.score, "item") else node_with_score.score, + } + + +def serialize_contexts(contexts): + return {key: [serialize_node_with_score(node) for node in nodes] for key, nodes in contexts.items()} diff --git a/EdgeCraftRAG/ui/vue/components.d.ts b/EdgeCraftRAG/ui/vue/components.d.ts index 64ca06335b..06d8f193b3 100644 --- a/EdgeCraftRAG/ui/vue/components.d.ts +++ b/EdgeCraftRAG/ui/vue/components.d.ts @@ -32,17 +32,22 @@ declare module 'vue' { AModal: typeof import('ant-design-vue/es')['Modal'] APagination: typeof import('ant-design-vue/es')['Pagination'] APopover: typeof import('ant-design-vue/es')['Popover'] + AProgress: typeof import('ant-design-vue/es')['Progress'] ARadio: typeof import('ant-design-vue/es')['Radio'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] + ARate: typeof import('ant-design-vue/es')['Rate'] ARow: typeof import('ant-design-vue/es')['Row'] ASelect: typeof import('ant-design-vue/es')['Select'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASlider: typeof import('ant-design-vue/es')['Slider'] ASpace: typeof import('ant-design-vue/es')['Space'] ASteps: typeof import('ant-design-vue/es')['Steps'] + ASwitch: typeof import('ant-design-vue/es')['Switch'] ATable: typeof import('ant-design-vue/es')['Table'] ATag: typeof import('ant-design-vue/es')['Tag'] + ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATooltip: typeof import('ant-design-vue/es')['Tooltip'] + AUpload: typeof import('ant-design-vue/es')['Upload'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] FormTooltip: typeof import('./src/components/FormTooltip.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/EdgeCraftRAG/ui/vue/package.json b/EdgeCraftRAG/ui/vue/package.json index 0c3e32bc11..29539d7c55 100644 --- a/EdgeCraftRAG/ui/vue/package.json +++ b/EdgeCraftRAG/ui/vue/package.json @@ -12,8 +12,10 @@ "@vueuse/i18n": "^4.0.0-beta.12", "ant-design-vue": "^4.0.0-rc.6", "axios": "^1.7.9", + "clipboard": "^2.0.11", "echarts": "^5.5.1", "event-source-polyfill": "^1.0.31", + "highlight.js": "^11.11.1", "http": "^0.0.1-security", "js-cookie": "^3.0.5", "lodash": "^4.17.21", diff --git a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.css b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.css index 0fd282ff5e..3174517c31 100644 --- a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.css +++ b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.css @@ -1,9 +1,9 @@ @font-face { font-family: "iconfont"; /* Project id 4784207 */ src: - url("iconfont.woff2?t=1739238081968") format("woff2"), - url("iconfont.woff?t=1739238081968") format("woff"), - url("iconfont.ttf?t=1739238081968") format("truetype"); + url("iconfont.woff2?t=1744699312006") format("woff2"), + url("iconfont.woff?t=1744699312006") format("woff"), + url("iconfont.ttf?t=1744699312006") format("truetype"); } .iconfont { @@ -14,6 +14,42 @@ -moz-osx-font-smoothing: grayscale; } +.icon-export:before { + content: "\e619"; +} + +.icon-rename:before { + content: "\e618"; +} + +.icon-delete:before { + content: "\e664"; +} + +.icon-setting1:before { + content: "\e61b"; +} + +.icon-upload:before { + content: "\e617"; +} + +.icon-clear:before { + content: "\e765"; +} + +.icon-copy-success:before { + content: "\e666"; +} + +.icon-copy:before { + content: "\e660"; +} + +.icon-subway:before { + content: "\e6ed"; +} + .icon-stop:before { content: "\e904"; } diff --git a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.js b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.js index 5c567a1196..f0bb06c334 100644 --- a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.js +++ b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.js @@ -2,67 +2,67 @@ // SPDX-License-Identifier: Apache-2.0 (window._iconfont_svg_string_4784207 = - ''), - ((l) => { - var a = (t = (t = document.getElementsByTagName("script"))[t.length - 1]).getAttribute("data-injectcss"), - t = t.getAttribute("data-disable-injectsvg"); - if (!t) { - var h, + ''), + ((h) => { + var l = (a = (a = document.getElementsByTagName("script"))[a.length - 1]).getAttribute("data-injectcss"), + a = a.getAttribute("data-disable-injectsvg"); + if (!a) { + var t, + c, i, o, e, - c, - v = function (a, t) { - t.parentNode.insertBefore(a, t); + v = function (l, a) { + a.parentNode.insertBefore(l, a); }; - if (a && !l.__iconfont__svg__cssinject__) { - l.__iconfont__svg__cssinject__ = !0; + if (l && !h.__iconfont__svg__cssinject__) { + h.__iconfont__svg__cssinject__ = !0; try { document.write( "", ); - } catch (a) { - console && console.log(a); + } catch (l) { + console && console.log(l); } } - (h = function () { - var a, - t = document.createElement("div"); - (t.innerHTML = l._iconfont_svg_string_4784207), - (t = t.getElementsByTagName("svg")[0]) && - (t.setAttribute("aria-hidden", "true"), - (t.style.position = "absolute"), - (t.style.width = 0), - (t.style.height = 0), - (t.style.overflow = "hidden"), - (t = t), - (a = document.body).firstChild ? v(t, a.firstChild) : a.appendChild(t)); + (t = function () { + var l, + a = document.createElement("div"); + (a.innerHTML = h._iconfont_svg_string_4784207), + (a = a.getElementsByTagName("svg")[0]) && + (a.setAttribute("aria-hidden", "true"), + (a.style.position = "absolute"), + (a.style.width = 0), + (a.style.height = 0), + (a.style.overflow = "hidden"), + (a = a), + (l = document.body).firstChild ? v(a, l.firstChild) : l.appendChild(a)); }), document.addEventListener ? ~["complete", "loaded", "interactive"].indexOf(document.readyState) - ? setTimeout(h, 0) - : ((i = function () { - document.removeEventListener("DOMContentLoaded", i, !1), h(); + ? setTimeout(t, 0) + : ((c = function () { + document.removeEventListener("DOMContentLoaded", c, !1), t(); }), - document.addEventListener("DOMContentLoaded", i, !1)) + document.addEventListener("DOMContentLoaded", c, !1)) : document.attachEvent && - ((o = h), - (e = l.document), - (c = !1), + ((i = t), + (o = h.document), + (e = !1), d(), - (e.onreadystatechange = function () { - "complete" == e.readyState && ((e.onreadystatechange = null), n()); + (o.onreadystatechange = function () { + "complete" == o.readyState && ((o.onreadystatechange = null), s()); })); } - function n() { - c || ((c = !0), o()); + function s() { + e || ((e = !0), i()); } function d() { try { - e.documentElement.doScroll("left"); - } catch (a) { + o.documentElement.doScroll("left"); + } catch (l) { return void setTimeout(d, 50); } - n(); + s(); } })(window); diff --git a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.json b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.json index f00702b38d..9caf20d219 100644 --- a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.json +++ b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.json @@ -5,6 +5,69 @@ "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "43924556", + "name": "export", + "font_class": "export", + "unicode": "e619", + "unicode_decimal": 58905 + }, + { + "icon_id": "43924554", + "name": "rename", + "font_class": "rename", + "unicode": "e618", + "unicode_decimal": 58904 + }, + { + "icon_id": "2570142", + "name": "delete", + "font_class": "delete", + "unicode": "e664", + "unicode_decimal": 58980 + }, + { + "icon_id": "13253937", + "name": "setting", + "font_class": "setting1", + "unicode": "e61b", + "unicode_decimal": 58907 + }, + { + "icon_id": "43796752", + "name": "upload", + "font_class": "upload", + "unicode": "e617", + "unicode_decimal": 58903 + }, + { + "icon_id": "42194548", + "name": "clear", + "font_class": "clear", + "unicode": "e765", + "unicode_decimal": 59237 + }, + { + "icon_id": "1198529", + "name": "copy-success", + "font_class": "copy-success", + "unicode": "e666", + "unicode_decimal": 58982 + }, + { + "icon_id": "9080698", + "name": "copy", + "font_class": "copy", + "unicode": "e660", + "unicode_decimal": 58976 + }, + { + "icon_id": "796912", + "name": "地铁", + "font_class": "subway", + "unicode": "e6ed", + "unicode_decimal": 59117 + }, { "icon_id": "42853460", "name": "stop", diff --git a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.ttf b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.ttf index 38480000d3..95023b5beb 100644 Binary files a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.ttf and b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.ttf differ diff --git a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff index c16ad1f171..1746c97f96 100644 Binary files a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff and b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff differ diff --git a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff2 b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff2 index 45249bf369..d871db62f2 100644 Binary files a/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff2 and b/EdgeCraftRAG/ui/vue/src/assets/iconFont/iconfont.woff2 differ diff --git a/EdgeCraftRAG/ui/vue/src/theme/markdown.less b/EdgeCraftRAG/ui/vue/src/theme/markdown.less index 918650ff33..f5377dcd48 100644 --- a/EdgeCraftRAG/ui/vue/src/theme/markdown.less +++ b/EdgeCraftRAG/ui/vue/src/theme/markdown.less @@ -19,6 +19,9 @@ padding: 0.2em 0.4em; white-space: break-spaces; } + pre { + margin-bottom: 0; + } h1, h2, h3, @@ -69,4 +72,76 @@ img[align="left"] { padding-right: 20px; } + table { + width: 100%; + margin-top: 16px; + border: 1px solid var(--border-main-color); + border-collapse: collapse; + color: var(--font-main-color); + } + table th { + background-color: var(--table-th-bg); + font-weight: 600; + text-align: left; + } + + table td, + table th { + border: 1px solid var(--border-main-color); + padding: 6px 13px; + margin: 0; + } + table td { + background-color: var(--table-td-bg); + } + table td > :last-child { + margin-bottom: 0; + } + + table tr { + font-size: 14px; + border-top: 1px solid var(--border-main-color); + } + + blockquote { + color: var(--blockquote-color); + border-left: 3px solid var(--bg-scrollbar); + margin: 8px 0px; + padding: 0px 10px; + } + .intel-highlighter { + width: 100%; + border-radius: 4px; + overflow: hidden; + margin-top: 16px; + margin-bottom: 12px; + .header-wrap { + align-items: center; + background-color: var(--code-header-bg); + color: var(--code-header-font); + display: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; + height: 32px; + padding: 0 14px; + .copy-icon { + display: block; + color: var(--font-main-color); + cursor: pointer; + } + .success-icon { + display: none; + color: var(--color-success); + } + } + .content-wrap { + overflow-x: auto; + background: var(--code-content-bg); + padding: 16px; + font-size: 13px; + margin-top: 0; + color: var(--font-main-color); + } + } } diff --git a/EdgeCraftRAG/ui/vue/src/theme/variables.less b/EdgeCraftRAG/ui/vue/src/theme/variables.less index e8914623af..fd3c73958c 100644 --- a/EdgeCraftRAG/ui/vue/src/theme/variables.less +++ b/EdgeCraftRAG/ui/vue/src/theme/variables.less @@ -6,18 +6,23 @@ --color-primary: #00377c; --color-primary-hover: #0054ae; --color-primaryBg: #e0eaff; + --color-second-primaryBg: #d4e1fd; --color-error: #ce0000; --color-error-hover: #ff5d52; + --color-errorBg: #ffa3a3; --color-info: #aaaaaa; + --color-infoBg: #ffffff; --color-success: #179958; - --color-warning: #faad14; - --color-big-icon: #111111; --color-successBg: #d6ffe8; + --color-second-successBg: #f0fdf4; + --color-warning: #faad14; + --color-second-warning: #854d0e; --color-warningBg: #feefd0; - --color-errorBg: #ffa3a3; - --color-infoBg: #ffffff; + --color-second-warningBg: #fefce8; + --color-big-icon: #111111; --bg-main-color: #f5f5f5; --bg-card-color: #f9f9f9; + --bg-second-card-bg: var(--color-white); --bg-loading-color: rgba(0, 0, 0, 0.45); --bg-content-color: var(--color-white); --font-main-color: #333333; @@ -31,15 +36,30 @@ 0px 10px 15px -3px rgba(0, 0, 0, 0.1); --menu-bg: var(--bg-main-color); --color-switch-theme: #e5e7eb; - + --think-done-icon: #356bfd; + --think-done-bg: linear-gradient(180deg, #f3f5fc 30%, #ffffff 100%); + --font-think-color: #5e5e5e; + --face-icon-bg: #a6a6a6; + --bg-switch: var(--color-primaryBg); //边框 --border-main-color: #e5e7eb; + --border-warning: var(--color-warningBg); + --border-success: var(--color-successBg); + --border-primary: var(--color-second-primaryBg); //黑色按钮 --bg-black-color: #434343; --bg-black-hover-color: #595959; --bg-black-active-color: #262626; --border-black-color: #434343; + + //md显示 + --code-header-bg: #dddddd; + --code-header-font: #2c2c36; + --code-content-bg: #f0f0f0; + --table-th-bg: #dddddd; + --table-td-bg: #f0f0f0; + --blockquote-color: var(--font-info-color); } [data-theme="dark"] { @@ -51,26 +71,41 @@ --font-text-color: #e9e9e9; --font-info-color: #aeaeae; --font-tip-color: #aeaeae; - --bg-scrollbar: #dddddd; - --bg-scrollbar-hover: #bbbbbb; + --bg-scrollbar: #595959; + --bg-scrollbar-hover: #666666; --color-primary: #0054ae; --color-primary-hover: #1668dc; --color-primaryBg: #95b5fa; + --color-second-primaryBg: #d4e1fd; --bg-box-shadow: rgba(109, 153, 233, 0.05); --bg-gradient-shadow: 0px 4px 6px -4px rgba(255, 255, 255, 0.1), 0px 5px 8px 1px rgba(255, 255, 255, 0.1); --menu-bg: #3e3e3e; --color-big-icon: #ffffff; + --bg-second-card-bg: #111111; --color-switch-theme: var(--color-primary-hover); - + --think-done-bg: linear-gradient(180deg, #32313a 30%, #2d2d2d 100%); + --font-think-color: #e0ecffcc; + --bg-switch: var(--bg-card-color); //边框 - --border-main-color: #2b2b2b; + --border-main-color: #3b3b3b; + --border-warning: #f8e9ca; + --border-success: #d7f8e8; + --border-primary: #d5daf8; //黑色按钮 --bg-black-color: #434343; --bg-black-hover-color: #595959; --bg-black-active-color: #262626; --border-black-color: #434343; + + //md显示 + --code-header-bg: #585a73; + --code-header-font: #fafafc; + --code-content-bg: #2c2c36; + --table-th-bg: #585a73; + --table-td-bg: #2c2c36; + --blockquote-color: var(--bg-scrollbar-hover); } @use "ant-design-vue/es/style/themes/default.less"; diff --git a/EdgeCraftRAG/ui/vue/src/utils/common.ts b/EdgeCraftRAG/ui/vue/src/utils/common.ts index efdf142be9..b06b0b4462 100644 --- a/EdgeCraftRAG/ui/vue/src/utils/common.ts +++ b/EdgeCraftRAG/ui/vue/src/utils/common.ts @@ -19,3 +19,11 @@ export const formatDecimals = (num: number, decimalPlaces: number = 2) => { const factor = Math.pow(10, decimalPlaces); return Math.round(num * factor) / factor; }; + +export const formatCapitalize = (string: string, start: number = 0, length: number = 1) => { + const end = start + length; + const part1 = string.slice(0, start); + const part2 = string.slice(start, end).toUpperCase(); + const part3 = string.slice(end); + return part1 + part2 + part3; +}; diff --git a/EdgeCraftRAG/ui/vue/src/utils/customRenderer.ts b/EdgeCraftRAG/ui/vue/src/utils/customRenderer.ts new file mode 100644 index 0000000000..5a19ade40a --- /dev/null +++ b/EdgeCraftRAG/ui/vue/src/utils/customRenderer.ts @@ -0,0 +1,128 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { marked } from "marked"; +import hljs from "highlight.js"; +import { formatCapitalize } from "./common"; +import ClipboardJS from "clipboard"; +import { message } from "ant-design-vue"; + +interface CodeRenderParams { + text: string; + lang?: string; +} + +class ClipboardManager { + private clipboard: ClipboardJS | null = null; + private observer: MutationObserver | null = null; + + constructor() { + this.autoInit(); + } + + private autoInit() { + if (typeof document === "undefined") return; + const init = () => { + this.init(".copy-btn"); + this.setupMutationObserver(); + }; + + if (document.readyState === "complete") { + init(); + } else { + document.addEventListener("DOMContentLoaded", init); + } + } + + private init(selector: string) { + this.destroy(); + + this.clipboard = new ClipboardJS(selector, { container: document.body }); + + this.clipboard.on("success", (e) => this.handleSuccess(e)); + this.clipboard.on("error", (e) => this.handleError(e)); + } + + private setupMutationObserver() { + this.observer = new MutationObserver((mutations) => { + const hasNewButtons = mutations.some((mutation) => + Array.from(mutation.addedNodes).some( + (node) => node instanceof HTMLElement && (node.matches(".copy-btn") || node.querySelector(".copy-btn")), + ), + ); + if (hasNewButtons) this.init(".copy-btn"); + }); + + this.observer.observe(document.body, { + childList: true, + subtree: true, + }); + } + + destroy() { + this.clipboard?.destroy(); + this.observer?.disconnect(); + this.clipboard = null; + this.observer = null; + } + + private handleSuccess(e: ClipboardJS.Event) { + e.clearSelection(); + message.success("Copy Successful !"); + const button = e.trigger as HTMLElement; + const copyIcon = button.querySelector(".copy-icon") as HTMLElement; + const successIcon = button.querySelector(".success-icon") as HTMLElement; + + copyIcon.style.display = "none"; + successIcon.style.display = "block"; + + let timeout = null; + if (timeout) clearTimeout(timeout); + + timeout = setTimeout(() => { + copyIcon.style.display = "block"; + successIcon.style.display = "none"; + }, 2000); + } + + private handleError(e: ClipboardJS.Event) { + message.error("Copy Failure !"); + } +} + +export const clipboardManager = new ClipboardManager(); + +const createCustomRenderer = () => { + const renderer = new marked.Renderer(); + + renderer.link = ({ href, title, text }) => { + return `${text}`; + }; + + renderer.code = ({ text, lang }: CodeRenderParams) => { + const language = hljs.getLanguage(lang || "") ? lang : "plaintext"; + const codeTitle = formatCapitalize(language || "Code"); + const codeHtml = hljs.highlight(text, { + language: language || "plaintext", + }).value; + const uniqueId = `code-${Date.now()}-${Math.random().toString(16).slice(2)}`; + + return ` +
${codeHtml}
+