Compare commits
31 Commits
c22081c5e9
...
feat/delet
| Author | SHA1 | Date | |
|---|---|---|---|
| feff8ea4d3 | |||
| 6abec8c7b7 | |||
| f9b3699bda | |||
| 8da1177bf3 | |||
| a85a044a9b | |||
| c58e3603de | |||
| 940584e1ea | |||
| 4037e9e2d7 | |||
| 4b2f6967dc | |||
| 79ef879b97 | |||
| 1df7462778 | |||
| 0bcdc615e3 | |||
| 5577fe17bb | |||
| 820ee2f848 | |||
| be8365c8cb | |||
| b1dd2442b8 | |||
| 9281d1d724 | |||
| 226bb46094 | |||
| 251289a340 | |||
| cca82c7885 | |||
| 3831de2849 | |||
| 0df529c4fd | |||
| 5c53d7f0e9 | |||
| ef2a0dc16e | |||
| 5ce420295b | |||
| 1d7f3d7626 | |||
| 9b0d2d5e01 | |||
| 27e3351b55 | |||
| de3e84aa4e | |||
| 8dc34ee435 | |||
| d8cd86361e |
1027
docs/数据流程详解.md
Normal file
1027
docs/数据流程详解.md
Normal file
File diff suppressed because it is too large
Load Diff
200
media/surfer/index.html
Normal file
200
media/surfer/index.html
Normal file
@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
<!-- Disable zooming: -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<head>
|
||||
<!-- change this to your project name -->
|
||||
<title>Surfer</title>
|
||||
|
||||
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
||||
<script type="module">
|
||||
import init from '/surfer.js';
|
||||
await init({module_or_path: '/surfer_bg.wasm'});
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
/*SURFER_SETUP_HOOKS*/
|
||||
</script>
|
||||
|
||||
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
||||
<base href="/" />
|
||||
|
||||
|
||||
<script>
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Setting error message")
|
||||
document.getElementById("error_message").innerHTML = msg
|
||||
document.getElementById("error_container").style.display = "block"
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||
|
||||
<style>
|
||||
html {
|
||||
/* Remove touch delay: */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Light mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #909090;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
/* Dark mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #404040;
|
||||
}
|
||||
}
|
||||
|
||||
/* Allow canvas to fill entire web page: */
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Make canvas fill entire document: */
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #f0f0f0;
|
||||
font-size: 24px;
|
||||
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------- */
|
||||
/* Loading animation from https://loading.io/css/ */
|
||||
.lds-dual-ring {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lds-dual-ring:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #fff;
|
||||
border-color: #fff transparent #fff transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: #ffffff;
|
||||
background-color: black;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#error_container a {
|
||||
color: #ff9999;
|
||||
}
|
||||
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
|
||||
|
||||
<body>
|
||||
<!-- The WASM code will resize the canvas dynamically -->
|
||||
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div id="error_container" style="display: none;">
|
||||
<h1>Sorry, Surfer crashed 🔥</h1>
|
||||
<p>
|
||||
Something caused Surfer to crash. Please report the error on
|
||||
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
|
||||
gitlab
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
|
||||
the crash and/or the steps to reproduce the crash.
|
||||
</p>
|
||||
<h3>
|
||||
Backtrace:
|
||||
</h3>
|
||||
<div class="error_container">
|
||||
<!-- This is filled in by javascript -->
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register the message listener system -->
|
||||
<script src="integration.js"></script>
|
||||
<script>
|
||||
register_message_listener()
|
||||
</script>
|
||||
|
||||
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
||||
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
||||
<script>
|
||||
// We disable caching during development so that we always view the latest version.
|
||||
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('sw.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
||||
65
media/surfer/integration.js
Normal file
65
media/surfer/integration.js
Normal file
@ -0,0 +1,65 @@
|
||||
// Web apps which integrate Surfer as an iframe can give commands to surfer via
|
||||
// the .postMessage [1] function on the iframe.
|
||||
//
|
||||
// For example, to tell Surfer to load waveforms from a URL, use
|
||||
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
|
||||
//
|
||||
// For more complex functionality, one can also inject any `Message` defined
|
||||
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
|
||||
// is not stable and may change at any time. If you add functionality via
|
||||
// these, make sure to test the new functionality when changing Surfer version.
|
||||
//
|
||||
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
|
||||
|
||||
function register_message_listener() {
|
||||
window.addEventListener("message", (event) => {
|
||||
// JSON decode the message
|
||||
const decoded = event.data
|
||||
|
||||
switch (decoded.command) {
|
||||
// Load a waveform from a URL. The format is inferred from the data.
|
||||
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
|
||||
|
||||
case 'LoadUrl': {
|
||||
const msg = {
|
||||
LoadWaveformFileFromUrl: [
|
||||
decoded.url,
|
||||
"Clear"
|
||||
]
|
||||
}
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ToggleMenu': {
|
||||
const msg = "ToggleMenu"
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
// Load waveform data directly from string content
|
||||
case 'LoadData': {
|
||||
const msg = {
|
||||
LoadFromData: [
|
||||
decoded.content,
|
||||
decoded.fileName || "waveform.vcd",
|
||||
"Clear"
|
||||
]
|
||||
}
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
// Inject any other message supported by Surfer in the surfer::Message enum.
|
||||
// NOTE: The API of these is unstable.
|
||||
case 'InjectMessage': {
|
||||
inject_message(decoded.message);
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unknown message.command ${decoded.command}`)
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
10
media/surfer/manifest.json
Normal file
10
media/surfer/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"display": "standalone",
|
||||
"id": "/index.html",
|
||||
"lang": "en-US",
|
||||
"name": "Surfer",
|
||||
"short_name": "surfer",
|
||||
"start_url": "./index.html",
|
||||
"theme_color": "white"
|
||||
}
|
||||
2227
media/surfer/surfer.js
Normal file
2227
media/surfer/surfer.js
Normal file
File diff suppressed because it is too large
Load Diff
200
media/surfer/surfer/index.html
Normal file
200
media/surfer/surfer/index.html
Normal file
@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
<!-- Disable zooming: -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<head>
|
||||
<!-- change this to your project name -->
|
||||
<title>Surfer</title>
|
||||
|
||||
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
||||
<script type="module">
|
||||
import init from '/surfer.js';
|
||||
await init({module_or_path: '/surfer_bg.wasm'});
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
/*SURFER_SETUP_HOOKS*/
|
||||
</script>
|
||||
|
||||
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
||||
<base href="/" />
|
||||
|
||||
|
||||
<script>
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Setting error message")
|
||||
document.getElementById("error_message").innerHTML = msg
|
||||
document.getElementById("error_container").style.display = "block"
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||
|
||||
<style>
|
||||
html {
|
||||
/* Remove touch delay: */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Light mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #909090;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
/* Dark mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #404040;
|
||||
}
|
||||
}
|
||||
|
||||
/* Allow canvas to fill entire web page: */
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Make canvas fill entire document: */
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #f0f0f0;
|
||||
font-size: 24px;
|
||||
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------- */
|
||||
/* Loading animation from https://loading.io/css/ */
|
||||
.lds-dual-ring {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lds-dual-ring:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #fff;
|
||||
border-color: #fff transparent #fff transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: #ffffff;
|
||||
background-color: black;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#error_container a {
|
||||
color: #ff9999;
|
||||
}
|
||||
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
|
||||
|
||||
<body>
|
||||
<!-- The WASM code will resize the canvas dynamically -->
|
||||
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div id="error_container" style="display: none;">
|
||||
<h1>Sorry, Surfer crashed 🔥</h1>
|
||||
<p>
|
||||
Something caused Surfer to crash. Please report the error on
|
||||
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
|
||||
gitlab
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
|
||||
the crash and/or the steps to reproduce the crash.
|
||||
</p>
|
||||
<h3>
|
||||
Backtrace:
|
||||
</h3>
|
||||
<div class="error_container">
|
||||
<!-- This is filled in by javascript -->
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register the message listener system -->
|
||||
<script src="integration.js"></script>
|
||||
<script>
|
||||
register_message_listener()
|
||||
</script>
|
||||
|
||||
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
||||
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
||||
<script>
|
||||
// We disable caching during development so that we always view the latest version.
|
||||
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('sw.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
||||
52
media/surfer/surfer/integration.js
Normal file
52
media/surfer/surfer/integration.js
Normal file
@ -0,0 +1,52 @@
|
||||
// Web apps which integrate Surfer as an iframe can give commands to surfer via
|
||||
// the .postMessage [1] function on the iframe.
|
||||
//
|
||||
// For example, to tell Surfer to load waveforms from a URL, use
|
||||
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
|
||||
//
|
||||
// For more complex functionality, one can also inject any `Message` defined
|
||||
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
|
||||
// is not stable and may change at any time. If you add functionality via
|
||||
// these, make sure to test the new functionality when changing Surfer version.
|
||||
//
|
||||
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
|
||||
|
||||
function register_message_listener() {
|
||||
window.addEventListener("message", (event) => {
|
||||
// JSON decode the message
|
||||
const decoded = event.data
|
||||
|
||||
switch (decoded.command) {
|
||||
// Load a waveform from a URL. The format is inferred from the data.
|
||||
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
|
||||
|
||||
case 'LoadUrl': {
|
||||
const msg = {
|
||||
LoadWaveformFileFromUrl: [
|
||||
decoded.url,
|
||||
"Clear"
|
||||
]
|
||||
}
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ToggleMenu': {
|
||||
const msg = "ToggleMenu"
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
// Inject any other message supported by Surfer in the surfer::Message enum.
|
||||
// NOTE: The API of these is unstable.
|
||||
case 'InjectMessage': {
|
||||
inject_message(decoded.message);
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unknown message.command ${decoded.command}`)
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
10
media/surfer/surfer/manifest.json
Normal file
10
media/surfer/surfer/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"display": "standalone",
|
||||
"id": "/index.html",
|
||||
"lang": "en-US",
|
||||
"name": "Surfer",
|
||||
"short_name": "surfer",
|
||||
"start_url": "./index.html",
|
||||
"theme_color": "white"
|
||||
}
|
||||
2227
media/surfer/surfer/surfer.js
Normal file
2227
media/surfer/surfer/surfer.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
media/surfer/surfer/surfer_bg.wasm
Normal file
BIN
media/surfer/surfer/surfer_bg.wasm
Normal file
Binary file not shown.
37
media/surfer/surfer/sw.js
Normal file
37
media/surfer/surfer/sw.js
Normal file
@ -0,0 +1,37 @@
|
||||
self.addEventListener("install", function () {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (event) {
|
||||
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then(function (response) {
|
||||
// It seems like we only need to set the headers for index.html
|
||||
// If you want to be on the safe side, comment this out
|
||||
// if (!response.url.includes("index.html")) return response;
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
|
||||
const moddedResponse = new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
|
||||
return moddedResponse;
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error(e);
|
||||
})
|
||||
);
|
||||
});
|
||||
BIN
media/surfer/surfer_bg.wasm
Normal file
BIN
media/surfer/surfer_bg.wasm
Normal file
Binary file not shown.
37
media/surfer/sw.js
Normal file
37
media/surfer/sw.js
Normal file
@ -0,0 +1,37 @@
|
||||
self.addEventListener("install", function () {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (event) {
|
||||
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then(function (response) {
|
||||
// It seems like we only need to set the headers for index.html
|
||||
// If you want to be on the safe side, comment this out
|
||||
// if (!response.url.includes("index.html")) return response;
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
|
||||
const moddedResponse = new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
|
||||
return moddedResponse;
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error(e);
|
||||
})
|
||||
);
|
||||
});
|
||||
15
package.json
15
package.json
@ -70,6 +70,18 @@
|
||||
"id": "iccoder",
|
||||
"label": "IC Coder"
|
||||
}
|
||||
],
|
||||
"customEditors": [
|
||||
{
|
||||
"viewType": "ic-coder.vcdViewer",
|
||||
"displayName": "VCD 波形查看器",
|
||||
"selector": [
|
||||
{
|
||||
"filenamePattern": "*.vcd"
|
||||
}
|
||||
],
|
||||
"priority": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
@ -101,7 +113,8 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"media",
|
||||
"tools"
|
||||
"tools",
|
||||
"src/assets"
|
||||
],
|
||||
"dependencies": {
|
||||
"@wavedrom/doppler": "^1.14.0",
|
||||
|
||||
BIN
rustup-init.exe
Normal file
BIN
rustup-init.exe
Normal file
Binary file not shown.
BIN
src/assets/titleIcon/PRO+.png
Normal file
BIN
src/assets/titleIcon/PRO+.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
BIN
src/assets/titleIcon/PRO.png
Normal file
BIN
src/assets/titleIcon/PRO.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/titleIcon/free.png
Normal file
BIN
src/assets/titleIcon/free.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
237
src/components/codeHighlight.ts
Normal file
237
src/components/codeHighlight.ts
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 代码高亮组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 使用 highlight.js 提供专业的代码语法高亮
|
||||
* - 支持多种编程语言(Verilog, JavaScript, Python 等)
|
||||
* - 提供行内代码和代码块的不同样式
|
||||
* - 自动检测语言类型
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取 highlight.js 的 CDN 链接
|
||||
*/
|
||||
export function getHighlightJsLinks(): string {
|
||||
return `
|
||||
<!-- Highlight.js CSS (VS Code Dark+ 主题) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css">
|
||||
<!-- Highlight.js 核心库 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<!-- Verilog 语言支持 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/verilog.min.js"></script>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代码高亮的样式
|
||||
*/
|
||||
export function getCodeHighlightStyles(): string {
|
||||
return `
|
||||
/* 代码块基础样式 */
|
||||
.segment-text pre {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
position: relative;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.segment-text pre code {
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
font-family: 'Courier New', Consolas, 'Monaco', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 行内代码样式 */
|
||||
.segment-text code:not(pre code) {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--vscode-textPreformat-foreground);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
font-family: 'Courier New', Consolas, 'Monaco', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 覆盖 highlight.js 的背景色,使用 VSCode 主题色 */
|
||||
.segment-text pre code.hljs {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 代码块语言标签 */
|
||||
.code-block-wrapper {
|
||||
position: relative;
|
||||
margin: -20px 0;
|
||||
}
|
||||
|
||||
.code-language-label {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 代码块复制按钮 */
|
||||
.code-copy-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.code-block-wrapper:hover .code-copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.code-copy-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.code-copy-btn.copied {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
/* 代码块滚动条样式 */
|
||||
.segment-text pre::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.segment-text pre::-webkit-scrollbar-track {
|
||||
background: var(--vscode-scrollbarSlider-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.segment-text pre::-webkit-scrollbar-thumb {
|
||||
background: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.segment-text pre::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代码高亮的脚本
|
||||
*/
|
||||
export function getCodeHighlightScript(): string {
|
||||
return `
|
||||
/**
|
||||
* 使用 highlight.js 进行代码高亮
|
||||
*/
|
||||
function highlightCodeBlocks() {
|
||||
// 等待 highlight.js 加载完成
|
||||
if (typeof hljs === 'undefined') {
|
||||
setTimeout(highlightCodeBlocks, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const codeBlocks = document.querySelectorAll('.segment-text pre code:not(.hljs)');
|
||||
codeBlocks.forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为代码块添加复制按钮
|
||||
*/
|
||||
function enhanceCodeBlocks() {
|
||||
const codeBlocks = document.querySelectorAll('.segment-text pre code');
|
||||
|
||||
codeBlocks.forEach((codeElement) => {
|
||||
const preElement = codeElement.parentElement;
|
||||
if (!preElement || preElement.classList.contains('enhanced')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记为已增强,避免重复处理
|
||||
preElement.classList.add('enhanced');
|
||||
|
||||
// 应用语法高亮
|
||||
if (typeof hljs !== 'undefined' && !codeElement.classList.contains('hljs')) {
|
||||
hljs.highlightElement(codeElement);
|
||||
}
|
||||
|
||||
// 创建包装器
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block-wrapper';
|
||||
preElement.parentNode.insertBefore(wrapper, preElement);
|
||||
wrapper.appendChild(preElement);
|
||||
|
||||
// 添加复制按钮
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'code-copy-btn';
|
||||
copyBtn.textContent = '复制';
|
||||
copyBtn.onclick = function() {
|
||||
const code = codeElement.textContent;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
copyBtn.textContent = '已复制';
|
||||
copyBtn.classList.add('copied');
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = '复制';
|
||||
copyBtn.classList.remove('copied');
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
wrapper.appendChild(copyBtn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 DOM 变化,自动增强新添加的代码块
|
||||
*/
|
||||
function observeCodeBlocks() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
enhanceCodeBlocks();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.getElementById('messages'), {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化代码块增强
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
enhanceCodeBlocks();
|
||||
observeCodeBlocks();
|
||||
});
|
||||
} else {
|
||||
enhanceCodeBlocks();
|
||||
observeCodeBlocks();
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -8,16 +8,23 @@ import * as vscode from "vscode";
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "dev";
|
||||
const CURRENT_ENV: Environment = "test";
|
||||
|
||||
/** 服务等级类型 */
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
|
||||
/** 配置项接口 */
|
||||
export interface IccoderConfig {
|
||||
/** 后端服务地址 */
|
||||
backendUrl: string;
|
||||
/** 后端服务地址(strangeLoop) */
|
||||
backendUrlStrongeLoop: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 用户ID(临时使用,后续对接认证) */
|
||||
userId: string;
|
||||
/** 服务等级 */
|
||||
serviceTier: ServiceTier;
|
||||
}
|
||||
|
||||
/** 环境配置 */
|
||||
@ -25,20 +32,26 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:2233",
|
||||
timeout: 300000, // 5分钟,与子智能体超时一致
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
timeout: 300000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max", // 默认使用 max
|
||||
},
|
||||
/** 测试服务器环境 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max",
|
||||
},
|
||||
/** 生产环境 */
|
||||
prod: {
|
||||
backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址
|
||||
backendUrl: "https://api.iccoder.com",
|
||||
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
serviceTier: "auto",
|
||||
},
|
||||
};
|
||||
|
||||
@ -67,3 +80,15 @@ export function getApiUrl(path: string): string {
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 StrangeLoop 服务 API 地址(用于用户信息等)
|
||||
*/
|
||||
export function getStrangeLoopApiUrl(path: string): string {
|
||||
const { backendUrlStrongeLoop } = getConfig();
|
||||
const baseUrl = backendUrlStrongeLoop.endsWith("/")
|
||||
? backendUrlStrongeLoop.slice(0, -1)
|
||||
: backendUrlStrongeLoop;
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
|
||||
@ -82,6 +82,11 @@ export const agentIconSvg = `
|
||||
*/
|
||||
export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`;
|
||||
|
||||
/**
|
||||
* Ask 模式图标 SVG
|
||||
*/
|
||||
export const askIconSvg = `<svg t="1767143500000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#8a8a8a"/><path d="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.5-111.6 40.7C369.2 344 352 380.7 352 420.4c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8 0-25.6 10.1-49.4 28.4-67.2 18.7-18.2 43.4-28.2 71.6-28.2s52.9 10 71.6 28.2c18.3 17.8 28.4 41.6 28.4 67.2 0 29.5-12.2 55.3-36.2 76.6-23.2 20.6-61.1 45.9-82.2 60.6-17.8 12.4-28.6 32.7-28.6 54.2V640c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-35.8c0-4.1 2.6-7.8 6.5-9.2 31.3-11.6 84.8-40.6 113.8-64.8 42.6-35.6 66.2-83.5 66.2-134.8 0-39.7-17.2-76.4-48.4-103.3z" fill="#8a8a8a"/><path d="M512 716m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#8a8a8a"/></svg>`;
|
||||
|
||||
/**
|
||||
* 保存知识库图标 SVG
|
||||
*/
|
||||
@ -165,3 +170,13 @@ export const stateTransitionIconSvg = `
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 用户提问图标 SVG
|
||||
*/
|
||||
export const userQuestionIconSvg = `<svg t="1767869230062" class="icon" viewBox="0 0 1068 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4819" width="14" height="14"><path d="M563.645217 578.782609c2.537739-35.350261 6.322087-58.189913 11.397566-68.518957 7.568696-15.449043 24.175304-34.370783 49.775304-56.631652 35.172174-30.72 58.546087-53.960348 70.121739-69.810087 11.575652-15.805217 17.408-36.418783 17.408-61.885217 0-41.939478-15.805217-76.399304-47.37113-103.379479-31.610435-26.980174-73.638957-40.470261-126.130087-40.47026-56.765217 0-101.376 15.760696-133.921392 47.282086C372.424348 256.934957 356.173913 298.562783 356.173913 350.386087h71.145739c1.335652-31.165217 6.811826-55.02887 16.384-71.590957 17.051826-29.740522 47.86087-44.610783 92.338087-44.610782 35.973565 0 61.796174 8.637217 77.378783 25.911652 15.582609 17.274435 23.373913 37.665391 23.373913 61.128348 0 16.784696-5.342609 32.990609-16.027826 48.573217-5.787826 8.904348-13.534609 17.363478-23.151305 25.555478l-31.966608 28.40487c-30.675478 27.113739-50.487652 51.155478-59.570087 72.125217-6.054957 13.979826-10.551652 41.627826-13.579131 82.899479h71.145739z m15.137392 89.043478a44.521739 44.521739 0 1 0-89.043479 0 44.521739 44.521739 0 0 0 89.043479 0z" fill="#8a8a8a" p-id="4820"></path><path d="M934.912 0h-801.391304a133.565217 133.565217 0 0 0-133.565218 133.565217v623.304348l0.222609 7.835826A133.565217 133.565217 0 0 0 133.565217 890.434783h222.608696v89.043478a44.521739 44.521739 0 0 0 64.556522 39.713391L675.661913 890.434783h259.294609a133.565217 133.565217 0 0 0 133.565217-133.565218V133.565217a133.565217 133.565217 0 0 0-133.565217-133.565217z m-801.391304 89.043478h801.391304a44.521739 44.521739 0 0 1 44.521739 44.521739v623.304348a44.521739 44.521739 0 0 1-44.521739 44.521739h-269.801739a44.521739 44.521739 0 0 0-20.034783 4.763826l-199.902608 100.930783V845.913043a44.521739 44.521739 0 0 0-44.52174-44.521739h-267.130434a44.521739 44.521739 0 0 1-44.521739-44.521739V133.565217a44.521739 44.521739 0 0 1 44.521739-44.521739z" fill="#8a8a8a" p-id="4821"></path></svg>`;
|
||||
|
||||
/**
|
||||
* 用户头像图标 SVG
|
||||
*/
|
||||
export const userAvatarIconSvg = `<svg t="1767947405083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4661" width="16" height="16"><path d="M515.541449 7.082899c-280.359429 0-508.458551 228.120391-508.458551 508.458551s228.120391 508.458551 508.458551 508.458551 508.458551-228.120391 508.458551-508.458551S795.900879 7.082899 515.541449 7.082899zM515.541449 981.864196c-257.132626 0-466.301477-209.190121-466.301477-466.322747 0-257.132626 209.168851-466.322747 466.301477-466.322747s466.301477 209.190121 466.301477 466.322747S772.674075 981.864196 515.541449 981.864196zM614.574414 524.177056 614.574414 524.177056c47.751075-31.96876 79.230625-86.398604 79.230625-148.187857 0-98.437405-79.804915-178.24232-178.24232-178.24232-98.437405 0-178.24232 79.804915-178.24232 178.24232 0 61.810523 31.479551 116.219097 79.251895 148.187857-100.266622 39.519598-171.244501 137.170014-171.244501 251.453545 0 0.23397 0 0.446669 0.02127 0.659369 0 0.04254-0.02127 0.10635-0.02127 0.14889 0 15.612155 12.65563 28.246516 28.267786 28.246516 15.590885 0 21.886796-12.63436 21.886796-28.246516 0-0.340319-0.08508-0.659369-0.10635-1.020958 0.10635-118.005774 102.159649-219.995264 220.207964-219.995264 118.112124 0 220.207964 102.095839 220.207964 220.207964 0 0.14889-1.467628 29.054774 21.971875 29.054774 15.505806 0 28.076356-12.57055 28.076356-28.055086 0-0.06381-0.02127-0.12762-0.02127-0.2127 0-0.25524 0.02127-0.510479 0.02127-0.786989C785.797645 661.34707 714.798496 563.696654 614.574414 524.177056zM515.541449 510.734437c-74.402343 0-134.723968-60.321625-134.723968-134.723968 0-74.423613 60.321625-134.723968 134.723968-134.723968 74.423613 0 134.723968 60.321625 134.723968 134.723968S589.943792 510.734437 515.541449 510.734437z" fill="currentColor" p-id="4662"></path></svg>`;
|
||||
|
||||
@ -1,13 +1,31 @@
|
||||
import * as vscode from "vscode";
|
||||
import { ICViewProvider } from "./views/ICViewProvider";
|
||||
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
||||
import { VCDViewerPanel } from "./panels/VCDViewerPanel";
|
||||
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||
import { VCDFileServer } from "./services/vcdFileServer";
|
||||
import { initUserService } from "./services/userService";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 初始化用户服务
|
||||
initUserService(context);
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||
vcdFileServer.start().then((port) => {
|
||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||
}).catch((error) => {
|
||||
console.error("启动 VCD 文件服务器失败:", error);
|
||||
});
|
||||
|
||||
// 在插件停用时关闭服务器
|
||||
context.subscriptions.push({
|
||||
dispose: () => vcdFileServer.stop()
|
||||
});
|
||||
|
||||
// 注册 Authentication Provider
|
||||
const authProvider = new ICCoderAuthenticationProvider(context);
|
||||
context.subscriptions.push(
|
||||
@ -68,7 +86,40 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath);
|
||||
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath, vcdFileServer);
|
||||
}
|
||||
);
|
||||
|
||||
// 注册命令:在浏览器中打开 VCD 波形查看器
|
||||
const openVCDViewerInBrowserCommand = vscode.commands.registerCommand(
|
||||
"ic-coder.openVCDViewerInBrowser",
|
||||
async (vcdFilePath?: string) => {
|
||||
if (!vcdFilePath) {
|
||||
const fileUri = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
filters: {
|
||||
"VCD 文件": ["vcd"],
|
||||
"所有文件": ["*"],
|
||||
},
|
||||
title: "选择 VCD 文件",
|
||||
});
|
||||
|
||||
if (fileUri && fileUri[0]) {
|
||||
vcdFilePath = fileUri[0].fsPath;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册文件到服务器
|
||||
const fileId = vcdFileServer.registerFile(vcdFilePath);
|
||||
const viewerUrl = vcdFileServer.getViewerUrl(fileId);
|
||||
|
||||
// 在默认浏览器中打开
|
||||
vscode.env.openExternal(vscode.Uri.parse(viewerUrl));
|
||||
vscode.window.showInformationMessage(`波形查看器已在浏览器中打开`);
|
||||
}
|
||||
);
|
||||
|
||||
@ -160,11 +211,15 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
viewProvider
|
||||
);
|
||||
|
||||
// 注册 VCD 自定义编辑器
|
||||
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
|
||||
|
||||
// 添加到订阅
|
||||
context.subscriptions.push(
|
||||
openPanelCommand,
|
||||
openChatCommand,
|
||||
openVCDViewerCommand,
|
||||
openVCDViewerInBrowserCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
// TODO: 等待重新实现这些命令
|
||||
@ -174,7 +229,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
// deleteSessionCommand,
|
||||
// clearHistoryCommand,
|
||||
// searchSessionCommand,
|
||||
viewRegistration
|
||||
viewRegistration,
|
||||
vcdEditorProvider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,38 @@ import { compactDialog } from "../services/apiClient";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { MessageType } from "../types/chatHistory";
|
||||
import { getCachedUserInfo } from "../services/userService";
|
||||
|
||||
/**
|
||||
* 获取会员等级图标 URI
|
||||
*/
|
||||
function getTierIconUri(
|
||||
webview: vscode.Webview,
|
||||
context: vscode.ExtensionContext,
|
||||
tierCode?: string
|
||||
): string | undefined {
|
||||
if (!tierCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tierIconMap: Record<string, string> = {
|
||||
'BASIC': 'free.png',
|
||||
'TRIAL': 'PRO-Try.png',
|
||||
'ADVANCED': 'PRO.png',
|
||||
'PROFESSIONAL': 'PRO+.png'
|
||||
};
|
||||
|
||||
const iconFile = tierIconMap[tierCode];
|
||||
if (!iconFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iconUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile)
|
||||
);
|
||||
|
||||
return iconUri.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并显示 IC 助手面板
|
||||
@ -108,6 +140,45 @@ export async function showICHelperPanel(
|
||||
maxIconUri.toString()
|
||||
);
|
||||
|
||||
// 获取并发送用户信息到 webview
|
||||
try {
|
||||
// 优先使用缓存的用户信息
|
||||
let userInfo = getCachedUserInfo();
|
||||
|
||||
if (userInfo) {
|
||||
// 使用缓存的用户信息
|
||||
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo);
|
||||
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode);
|
||||
panel.webview.postMessage({
|
||||
command: 'updateUserInfo',
|
||||
userInfo: {
|
||||
userId: userInfo.userId,
|
||||
nickname: userInfo.nickname,
|
||||
username: userInfo.username
|
||||
},
|
||||
tierIconUrl: tierIconUrl
|
||||
});
|
||||
} else {
|
||||
// 如果没有缓存,从 session 中获取
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (session) {
|
||||
console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account);
|
||||
panel.webview.postMessage({
|
||||
command: 'updateUserInfo',
|
||||
userInfo: {
|
||||
userId: session.account.id,
|
||||
nickname: session.account.label,
|
||||
username: session.account.label
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ICHelperPanel] 获取用户信息失败:', error);
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (message) => {
|
||||
@ -141,11 +212,15 @@ export async function showICHelperPanel(
|
||||
// 切换到当前面板的任务上下文
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
|
||||
// 显示进度条
|
||||
panel.webview.postMessage({ type: 'showProgress' });
|
||||
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode
|
||||
message.mode,
|
||||
message.model // 传递服务等级
|
||||
);
|
||||
break;
|
||||
case "readFile":
|
||||
@ -172,12 +247,9 @@ export async function showICHelperPanel(
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "openWaveformViewer":
|
||||
// 打开波形查看器
|
||||
// 在新列中打开波形查看器
|
||||
if (message.vcdFilePath) {
|
||||
VCDViewerPanel.createOrShow(
|
||||
context.extensionUri,
|
||||
message.vcdFilePath
|
||||
);
|
||||
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath);
|
||||
}
|
||||
break;
|
||||
case "getVCDInfo":
|
||||
@ -278,6 +350,109 @@ export async function showICHelperPanel(
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 添加文件上下文 - 显示工作区文件列表
|
||||
case "addContextFile":
|
||||
{
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
break;
|
||||
}
|
||||
|
||||
// 获取工作区所有文件
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*",
|
||||
"**/node_modules/**"
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFileList",
|
||||
files: files.map((uri) => ({
|
||||
path: uri.fsPath,
|
||||
relativePath: vscode.workspace.asRelativePath(uri),
|
||||
})),
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 添加文件夹上下文 - 显示工作区文件夹列表
|
||||
case "addContextFolder":
|
||||
{
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
break;
|
||||
}
|
||||
|
||||
// 获取工作区所有文件夹
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const folders: Array<{ path: string; relativePath: string }> = [];
|
||||
|
||||
function scanFolders(dir: string, baseDir: string) {
|
||||
try {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const item of items) {
|
||||
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
|
||||
const fullPath = path.join(dir, item.name);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
folders.push({ path: fullPath, relativePath });
|
||||
scanFolders(fullPath, baseDir);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("扫描文件夹失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFolderList",
|
||||
folders: folders,
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 添加图片上下文
|
||||
case "addContextImage":
|
||||
{
|
||||
const imageUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择图片",
|
||||
filters: {
|
||||
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
},
|
||||
});
|
||||
if (imageUris && imageUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextImagesSelected",
|
||||
images: imageUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 添加文档库上下文
|
||||
case "addContextDocument":
|
||||
{
|
||||
const docUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择文档",
|
||||
filters: {
|
||||
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
||||
"所有文件": ["*"],
|
||||
},
|
||||
});
|
||||
if (docUris && docUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextDocumentsSelected",
|
||||
documents: docUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 新增:检查工作区状态
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
|
||||
@ -1,19 +1,77 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { VCDFileServer } from "../services/vcdFileServer";
|
||||
|
||||
/**
|
||||
* VCD 波形查看器面板
|
||||
* VCD 波形查看器自定义编辑器提供者
|
||||
*/
|
||||
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
|
||||
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable {
|
||||
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
|
||||
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
||||
"ic-coder.vcdViewer",
|
||||
provider,
|
||||
{
|
||||
webviewOptions: {
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
return providerRegistration;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly vcdFileServer: VCDFileServer
|
||||
) {}
|
||||
|
||||
async openCustomDocument(
|
||||
uri: vscode.Uri,
|
||||
openContext: vscode.CustomDocumentOpenContext,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.CustomDocument> {
|
||||
return {
|
||||
uri,
|
||||
dispose: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
async resolveCustomEditor(
|
||||
document: vscode.CustomDocument,
|
||||
webviewPanel: vscode.WebviewPanel,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<void> {
|
||||
webviewPanel.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [this.context.extensionUri],
|
||||
};
|
||||
|
||||
// 使用公共工厂方法创建 VCD 查看器实例
|
||||
VCDViewerPanel.createFromWebviewPanel(
|
||||
webviewPanel,
|
||||
this.context.extensionUri,
|
||||
document.uri.fsPath,
|
||||
this.vcdFileServer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VCD 波形查看器面板 (使用 Surfer)
|
||||
*/
|
||||
export class VCDViewerPanel {
|
||||
public static currentPanel: VCDViewerPanel | undefined;
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private readonly _extensionUri: vscode.Uri;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _currentVcdPath: string | undefined;
|
||||
private _vcdFileServer: VCDFileServer | undefined;
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
|
||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
|
||||
this._panel = panel;
|
||||
this._extensionUri = extensionUri;
|
||||
this._vcdFileServer = vcdFileServer;
|
||||
|
||||
// 设置初始 HTML 内容
|
||||
this._panel.webview.html = this._getLoadingHtml();
|
||||
@ -24,12 +82,20 @@ export class VCDViewerPanel {
|
||||
// 监听来自 webview 的消息
|
||||
this._panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
console.log("[VCDViewerPanel] 收到消息:", message);
|
||||
switch (message.command) {
|
||||
case "loadVCD":
|
||||
if (message.filePath) {
|
||||
this.loadVCDFile(message.filePath);
|
||||
}
|
||||
break;
|
||||
case "loaded":
|
||||
// Surfer iframe 加载完成,发送 VCD 文件
|
||||
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
|
||||
if (this._currentVcdPath) {
|
||||
this.sendVcdToSurfer(this._currentVcdPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
null,
|
||||
@ -40,8 +106,9 @@ export class VCDViewerPanel {
|
||||
/**
|
||||
* 创建或显示 VCD 查看器面板
|
||||
*/
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string) {
|
||||
const column = vscode.ViewColumn.One;
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||
// 在当前活动编辑器旁边打开新列
|
||||
const column = vscode.ViewColumn.Beside;
|
||||
|
||||
// 如果已经有面板打开,则显示它
|
||||
if (VCDViewerPanel.currentPanel) {
|
||||
@ -64,7 +131,7 @@ export class VCDViewerPanel {
|
||||
}
|
||||
);
|
||||
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri);
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||
|
||||
// 如果提供了 VCD 文件路径,加载它
|
||||
if (vcdFilePath) {
|
||||
@ -72,23 +139,44 @@ export class VCDViewerPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已有的 webview panel 创建 VCD 查看器(用于自定义编辑器)
|
||||
*/
|
||||
public static createFromWebviewPanel(
|
||||
panel: vscode.WebviewPanel,
|
||||
extensionUri: vscode.Uri,
|
||||
vcdFilePath: string,
|
||||
vcdFileServer?: VCDFileServer
|
||||
) {
|
||||
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||
viewer.loadVCDFile(vcdFilePath);
|
||||
return viewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 VCD 文件
|
||||
*/
|
||||
public loadVCDFile(vcdFilePath: string) {
|
||||
try {
|
||||
console.log("[VCDViewerPanel] 开始加载 VCD 文件:", vcdFilePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前 VCD 路径
|
||||
this._currentVcdPath = vcdFilePath;
|
||||
console.log("[VCDViewerPanel] VCD 路径已保存:", this._currentVcdPath);
|
||||
|
||||
// 更新面板标题
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
this._panel.title = `VCD 波形查看器 - ${fileName}`;
|
||||
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
|
||||
|
||||
// 设置 HTML 内容
|
||||
this._panel.webview.html = this._getWebviewContent(vcdFilePath);
|
||||
this._panel.webview.html = this._getWebviewContent();
|
||||
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
@ -96,6 +184,104 @@ export class VCDViewerPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||||
*/
|
||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||
try {
|
||||
// 读取 VCD 文件
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||
const lines = buffer.split('\n');
|
||||
|
||||
const scopeNames: string[] = [];
|
||||
let scopeDepth = 0;
|
||||
const scopeStack: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// 遇到 $enddefinitions 就停止解析
|
||||
if (trimmed.startsWith('$enddefinitions')) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 查找 $scope 定义
|
||||
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||||
if (scopeMatch) {
|
||||
const scopeType = scopeMatch[1];
|
||||
const scopeName = scopeMatch[2];
|
||||
|
||||
// 记录顶层 module (depth = 0)
|
||||
if (scopeDepth === 0 && scopeType === 'module') {
|
||||
scopeStack.push(scopeName);
|
||||
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
||||
}
|
||||
// 记录顶层下的直接子模块 (depth = 1)
|
||||
else if (scopeDepth === 1 && scopeType === 'module') {
|
||||
const fullPath = [...scopeStack, scopeName];
|
||||
scopeNames.push(fullPath.join('.'));
|
||||
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
|
||||
}
|
||||
|
||||
scopeDepth++;
|
||||
}
|
||||
|
||||
// 遇到 $upscope 减少深度
|
||||
if (trimmed.startsWith('$upscope')) {
|
||||
scopeDepth--;
|
||||
if (scopeDepth === 0) {
|
||||
scopeStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopeNames;
|
||||
} catch (error) {
|
||||
console.error("[VCDViewerPanel] 解析 VCD 文件失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 VCD 文件到 Surfer
|
||||
*/
|
||||
private sendVcdToSurfer(vcdFilePath: string) {
|
||||
try {
|
||||
console.log("[VCDViewerPanel] 准备发送 VCD 到 Surfer:", vcdFilePath);
|
||||
|
||||
if (!this._vcdFileServer) {
|
||||
throw new Error("VCD 文件服务器未初始化");
|
||||
}
|
||||
|
||||
// 解析 VCD 文件获取根模块名称
|
||||
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||||
console.log("[VCDViewerPanel] 解析到的作用域名称:", scopeNames);
|
||||
|
||||
// 注册文件到 HTTP 服务器
|
||||
const fileId = this._vcdFileServer.registerFile(vcdFilePath);
|
||||
const httpUrl = this._vcdFileServer.getFileUrl(fileId);
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
|
||||
console.log("[VCDViewerPanel] 文件名:", fileName);
|
||||
console.log("[VCDViewerPanel] HTTP URL:", httpUrl);
|
||||
|
||||
// 使用 LoadUrl 命令通过 HTTP 加载文件
|
||||
this._panel.webview.postMessage({
|
||||
command: "loadVcdUrl",
|
||||
url: httpUrl,
|
||||
fileName: fileName,
|
||||
scopeNames: scopeNames, // 传递解析到的作用域名称
|
||||
});
|
||||
|
||||
console.log("[VCDViewerPanel] 已发送 loadVcdUrl 消息到 webview");
|
||||
} catch (error) {
|
||||
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
||||
vscode.window.showErrorMessage(
|
||||
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
@ -163,188 +349,239 @@ export class VCDViewerPanel {
|
||||
/**
|
||||
* 获取 Webview 的 HTML 内容
|
||||
*/
|
||||
private _getWebviewContent(vcdFilePath: string): string {
|
||||
// 获取资源 URI
|
||||
const vcdromJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcdrom.js")
|
||||
private _getWebviewContent(): string {
|
||||
// 获取 surfer 资源 URI
|
||||
const surferJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
|
||||
);
|
||||
const vcdWasmUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcd.wasm")
|
||||
const surferWasmUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
|
||||
);
|
||||
const fontRegularUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Regular.woff2")
|
||||
const integrationJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
|
||||
);
|
||||
const fontObliqueUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Oblique.woff2")
|
||||
);
|
||||
const fontItalicUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Italic.woff2")
|
||||
);
|
||||
|
||||
// 读取 VCD 文件内容并转换为 base64
|
||||
const vcdContent = fs.readFileSync(vcdFilePath, "utf-8");
|
||||
const vcdBase64 = Buffer.from(vcdContent).toString("base64");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${this._panel.webview.cspSource}; style-src 'unsafe-inline' ${this._panel.webview.cspSource}; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; img-src ${this._panel.webview.cspSource} data:; connect-src ${this._panel.webview.cspSource};">
|
||||
<title>VCD 波形查看器</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
|
||||
<title>Surfer 波形查看器</title>
|
||||
|
||||
<script>
|
||||
// 获取 VS Code API(只能调用一次)
|
||||
const vscode = acquireVsCodeApi();
|
||||
window.vscode = vscode;
|
||||
window.surferReady = false;
|
||||
window.pendingVcdData = null;
|
||||
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Surfer error:", msg);
|
||||
document.getElementById("error_message").innerHTML = msg;
|
||||
document.getElementById("error_container").style.display = "block";
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
|
||||
// 加载 VCD URL 的函数
|
||||
function loadVcdUrl(data) {
|
||||
try {
|
||||
console.log('[Webview] ========== 开始加载 VCD URL ==========');
|
||||
console.log('[Webview] URL:', data.url);
|
||||
console.log('[Webview] Scope names from VCD:', data.scopeNames);
|
||||
|
||||
// 使用 setTimeout 确保 Surfer 完全准备好
|
||||
setTimeout(() => {
|
||||
console.log('[Webview] 通过 postMessage 发送 LoadUrl 命令');
|
||||
|
||||
// 使用 integration.js 提供的标准 LoadUrl 命令
|
||||
window.postMessage({
|
||||
command: 'LoadUrl',
|
||||
url: data.url
|
||||
}, '*');
|
||||
|
||||
console.log('[Webview] ✅ 已发送 LoadUrl 命令');
|
||||
|
||||
// 等待文件加载完成后,自动添加所有信号
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('[Webview] Attempting to add all signals automatically');
|
||||
|
||||
// 使用从 VCD 文件解析出来的作用域名称
|
||||
let scopeNamesToTry = [];
|
||||
|
||||
if (data.scopeNames && data.scopeNames.length > 0) {
|
||||
// 使用解析出来的实际子模块路径(例如 "tb.dut")
|
||||
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||||
console.log('[Webview] Using parsed scope names:', scopeNamesToTry);
|
||||
} else {
|
||||
// 回退到常见的根作用域名称
|
||||
scopeNamesToTry = [
|
||||
['top'],
|
||||
['testbench'],
|
||||
['tb'],
|
||||
['test'],
|
||||
['dut']
|
||||
];
|
||||
console.log('[Webview] Using fallback scope names');
|
||||
}
|
||||
|
||||
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||||
const scopeName = scopeNamesToTry[i];
|
||||
try {
|
||||
const addScopeMsg = {
|
||||
"AddScope": [
|
||||
{
|
||||
"strs": scopeName,
|
||||
"id": {"Wellen": i + 1}
|
||||
},
|
||||
true // 递归添加子模块的所有信号
|
||||
]
|
||||
};
|
||||
window.inject_message(JSON.stringify(addScopeMsg));
|
||||
console.log('[Webview] Sent AddScope for: ' + scopeName.join('.') + ' (recursive)');
|
||||
} catch (e) {
|
||||
console.log('[Webview] Failed for scope: ' + scopeName.join('.'), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 等待信号加载完成后,自动缩放到全部时间范围
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ZoomToFit"));
|
||||
console.log('[Webview] Sent ZoomToFit command');
|
||||
} catch (e) {
|
||||
console.log('[Webview] ZoomToFit failed:', e);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Webview] Failed to add signals:', e);
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Webview] ❌ 加载 VCD 失败:', error);
|
||||
on_surfer_error(error.message + '\\n' + error.stack);
|
||||
}
|
||||
}
|
||||
window.loadVcdUrl = loadVcdUrl;
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
console.log('[Webview] 开始初始化 Surfer...');
|
||||
import init from '${surferJsUri}';
|
||||
await init({module_or_path: '${surferWasmUri}'});
|
||||
console.log('[Webview] Surfer WASM 已加载');
|
||||
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '${surferJsUri}';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
|
||||
console.log('[Webview] Surfer 函数已导入,inject_message 类型:', typeof window.inject_message);
|
||||
|
||||
// 等待一小段时间确保 Surfer 完全初始化
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
window.surferReady = true;
|
||||
console.log('[Webview] Surfer 已完全初始化并准备就绪');
|
||||
|
||||
// 关闭 Surfer 的日志面板(如果打开的话)
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ToggleLogs"));
|
||||
console.log('[Webview] 已发送关闭日志面板命令');
|
||||
} catch (e) {
|
||||
console.log('[Webview] 关闭日志面板失败:', e);
|
||||
}
|
||||
|
||||
// 如果有待处理的 VCD 数据,现在加载它
|
||||
if (window.pendingVcdData) {
|
||||
console.log('[Webview] 发现待处理的 VCD 数据,立即加载');
|
||||
loadVcdUrl(window.pendingVcdData);
|
||||
window.pendingVcdData = null;
|
||||
} else {
|
||||
console.log('[Webview] 没有待处理的 VCD 数据');
|
||||
}
|
||||
|
||||
// 通知 VS Code surfer 已加载完成
|
||||
console.log('[Webview] 发送 loaded 消息到 VS Code');
|
||||
window.vscode.postMessage({ command: 'loaded' });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Iosevka Drom Web';
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
src: url('${fontRegularUri}') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka Drom Web';
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-style: oblique;
|
||||
src: url('${fontObliqueUri}') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka Drom Web';
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-style: italic;
|
||||
src: url('${fontItalicUri}') format('woff2');
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Iosevka Drom Web', monospace;
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-editor-background);
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
#waveform-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#waveform1 {
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid var(--vscode-progressBar-background);
|
||||
border-top: 4px solid var(--vscode-progressBar-foreground);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: var(--vscode-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
border-radius: 4px;
|
||||
margin: 20px;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
<script src="${vcdromJsUri}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="waveform-container">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载 VCD 波形...</p>
|
||||
</div>
|
||||
<div id="waveform1"></div>
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div id="error_container" style="display: none;">
|
||||
<h3>❌ Surfer 加载失败</h3>
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
|
||||
<script src="${integrationJsUri}"></script>
|
||||
<script>
|
||||
(async function() {
|
||||
try {
|
||||
// 设置 WASM 文件路径
|
||||
window.wasmBinaryFile = '${vcdWasmUri}';
|
||||
register_message_listener();
|
||||
|
||||
// 解码 base64 VCD 内容
|
||||
const vcdBase64 = '${vcdBase64}';
|
||||
const vcdContent = atob(vcdBase64);
|
||||
console.log('[Webview] 注册 VS Code 消息监听器');
|
||||
// 监听来自 VS Code 扩展的消息(使用 vscode API)
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
// 隐藏加载提示
|
||||
document.querySelector('.loading').style.display = 'none';
|
||||
// 检查是否来自 VS Code
|
||||
if (message.command === 'loadVcdUrl') {
|
||||
console.log('[Webview] 收到 VS Code 消息,命令:', message.command);
|
||||
console.log('[Webview] Surfer 就绪状态:', window.surferReady);
|
||||
|
||||
// 创建一个函数来提供 VCD 数据流
|
||||
const vcdProvider = async (handler) => {
|
||||
// 将 VCD 内容转换为 Uint8Array
|
||||
const encoder = new TextEncoder();
|
||||
const vcdData = encoder.encode(vcdContent);
|
||||
|
||||
// 创建一个 ReadableStream reader
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(vcdData);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
// 调用 handler 并传递 reader
|
||||
await handler([{
|
||||
key: 'local',
|
||||
value: 'waveform.vcd',
|
||||
format: 'raw',
|
||||
baseName: 'waveform.vcd',
|
||||
ext: 'vcd',
|
||||
reader: reader
|
||||
}]);
|
||||
};
|
||||
|
||||
// 初始化 VCDrom,使用函数回调方式
|
||||
if (typeof VCDrom === 'function') {
|
||||
await VCDrom('waveform1', vcdProvider);
|
||||
if (window.surferReady) {
|
||||
// Surfer 已就绪,立即加载
|
||||
loadVcdUrl(message);
|
||||
} else {
|
||||
throw new Error('VCDrom 未正确加载');
|
||||
// Surfer 未就绪,保存数据等待加载
|
||||
console.log('[Webview] Surfer 未就绪,保存数据待加载');
|
||||
window.pendingVcdData = message;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载 VCD 波形失败:', error);
|
||||
document.getElementById('waveform-container').innerHTML =
|
||||
'<div class="error-message">' +
|
||||
'<h3>❌ 加载 VCD 波形失败</h3>' +
|
||||
'<p>' + error.message + '</p>' +
|
||||
'<p style="margin-top: 10px;">请确保 VCD 文件格式正确。</p>' +
|
||||
'<pre style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.1); overflow: auto;">' + error.stack + '</pre>' +
|
||||
'</div>';
|
||||
}
|
||||
})();
|
||||
}, true); // 使用捕获阶段,优先于 integration.js 的监听器
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
@ -6,7 +6,7 @@ import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
@ -213,3 +213,14 @@ export function createSystemErrorResult(id: number, code: number, message: strin
|
||||
error: { code, message }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||
console.log('[API] 获取用户信息');
|
||||
return request<UserInfoResponse>('/system/user/getInfo', {
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from '
|
||||
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
|
||||
import { userInteractionManager } from './userInteraction';
|
||||
import { getConfig } from '../config/settings';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
|
||||
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
||||
|
||||
@ -316,7 +316,8 @@ export class DialogSession {
|
||||
async sendMessage(
|
||||
message: string,
|
||||
callbacks: DialogCallbacks,
|
||||
mode?: RunMode
|
||||
mode?: RunMode,
|
||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||
): Promise<void> {
|
||||
if (this.isActive) {
|
||||
callbacks.onError?.('当前有对话正在进行中');
|
||||
@ -344,6 +345,7 @@ export class DialogSession {
|
||||
message,
|
||||
userId: config.userId,
|
||||
mode: mode || 'agent',
|
||||
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
||||
compactedData: compactedData || undefined,
|
||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||
knowledgeData: knowledgeData || undefined
|
||||
|
||||
@ -2,6 +2,7 @@ import * as vscode from "vscode";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||
|
||||
/**
|
||||
* IC Coder Authentication Provider
|
||||
@ -62,13 +63,16 @@ export class ICCoderAuthenticationProvider
|
||||
try {
|
||||
const token = await this.login();
|
||||
|
||||
// 获取到 token 后立即调用用户信息接口
|
||||
const userInfo = await onTokenReceived(token);
|
||||
|
||||
// 创建会话
|
||||
const session: vscode.AuthenticationSession = {
|
||||
id: this.generateSessionId(),
|
||||
accessToken: token,
|
||||
account: {
|
||||
id: "iccoder-user",
|
||||
label: "IC Coder 用户",
|
||||
id: userInfo?.userId || "iccoder-user",
|
||||
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
||||
},
|
||||
scopes: [...scopes],
|
||||
};
|
||||
@ -109,6 +113,9 @@ export class ICCoderAuthenticationProvider
|
||||
this._sessions.splice(sessionIndex, 1);
|
||||
await this.saveSessions();
|
||||
|
||||
// 清除用户信息缓存
|
||||
await clearUserInfo();
|
||||
|
||||
// 触发会话变化事件
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
|
||||
345
src/services/userService.ts
Normal file
345
src/services/userService.ts
Normal file
@ -0,0 +1,345 @@
|
||||
/**
|
||||
* 用户服务
|
||||
* 管理用户信息和认证相关的 API 调用
|
||||
*/
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import * as vscode from 'vscode';
|
||||
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
||||
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
*/
|
||||
interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
timeout?: number;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求(带 token)
|
||||
*/
|
||||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getStrangeLoopApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
// 如果有 token,添加到请求头
|
||||
if (options.token) {
|
||||
headers['Authorization'] = `Bearer ${options.token}`;
|
||||
}
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method,
|
||||
headers,
|
||||
timeout: options.timeout || timeout
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(`[HTTP] 响应状态码: ${res.statusCode}`);
|
||||
console.log(`[HTTP] 响应内容: ${data}`);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as T);
|
||||
} else {
|
||||
reject(new Error(json.error || json.message || json.msg || `HTTP ${res.statusCode}`));
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果不是 JSON,直接返回原始内容
|
||||
reject(new Error(`解析响应失败 (${res.statusCode}): ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息数据结构(实际返回的数据)
|
||||
*/
|
||||
export interface UserInfo {
|
||||
userId: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
phonenumber?: string;
|
||||
avatar?: string;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
// 会员信息
|
||||
membership?: {
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
remainingDays?: number;
|
||||
monthlyCredits?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||
const apiPath = '/system/user/getInfo';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取用户信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<UserInfoResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.user) {
|
||||
const user = response.user;
|
||||
return {
|
||||
userId: String(user.userId),
|
||||
username: user.userName,
|
||||
nickname: user.nickName,
|
||||
email: user.email,
|
||||
phonenumber: user.phonenumber,
|
||||
avatar: user.avatar,
|
||||
roles: response.roles,
|
||||
permissions: response.permissions,
|
||||
createTime: user.createTime,
|
||||
loginDate: user.loginDate
|
||||
};
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取用户信息失败:', response);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户会员信息
|
||||
* GET /strangeloop/api/membership/current
|
||||
*/
|
||||
export async function getMembershipInfo(token: string): Promise<MultiMembershipVO | null> {
|
||||
const apiPath = '/strangeloop/api/membership/current';
|
||||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||
console.log('[UserService] 获取会员信息');
|
||||
console.log('[UserService] 请求地址:', fullUrl);
|
||||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||
|
||||
try {
|
||||
const response = await request<MembershipResponse>(apiPath, {
|
||||
method: 'GET',
|
||||
token
|
||||
});
|
||||
|
||||
// 处理响应数据 - 检查 code 是否为 200
|
||||
if (response.code === 200 && response.data) {
|
||||
console.log('[UserService] 会员信息获取成功:', response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.error('[UserService] 获取会员信息失败:', response);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 请求会员信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级映射
|
||||
*/
|
||||
const TIER_LEVEL_MAP: Record<string, number> = {
|
||||
'BASIC': 1,
|
||||
'TRIAL': 2,
|
||||
'ADVANCED': 3,
|
||||
'PROFESSIONAL': 4
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取最高等级的会员信息
|
||||
*/
|
||||
function getHighestTierMembership(allMemberships?: MembershipItemVO[]): MembershipItemVO | null {
|
||||
if (!allMemberships || allMemberships.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 按等级排序,获取最高等级
|
||||
return allMemberships.reduce((highest, current) => {
|
||||
const currentLevel = TIER_LEVEL_MAP[current.tierCode] || 0;
|
||||
const highestLevel = TIER_LEVEL_MAP[highest.tierCode] || 0;
|
||||
return currentLevel > highestLevel ? current : highest;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当获取到 token 时自动调用此函数
|
||||
* 用于在登录成功后立即获取用户信息
|
||||
*/
|
||||
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
console.log('[UserService] Token 已获取,正在获取用户信息和会员信息...');
|
||||
|
||||
// 并行获取用户信息和会员信息
|
||||
const [userInfo, membershipInfo] = await Promise.all([
|
||||
getUserInfo(token),
|
||||
getMembershipInfo(token)
|
||||
]);
|
||||
|
||||
if (!userInfo) {
|
||||
console.warn('[UserService] 未能获取到用户信息');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 打印用户信息到控制台
|
||||
console.log('='.repeat(60));
|
||||
console.log('用户信息详情:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`用户ID: ${userInfo.userId}`);
|
||||
console.log(`用户名: ${userInfo.username}`);
|
||||
console.log(`昵称: ${userInfo.nickname}`);
|
||||
if (userInfo.email) {
|
||||
console.log(`邮箱: ${userInfo.email}`);
|
||||
}
|
||||
if (userInfo.phonenumber) {
|
||||
console.log(`手机号: ${userInfo.phonenumber}`);
|
||||
}
|
||||
if (userInfo.avatar) {
|
||||
console.log(`头像: ${userInfo.avatar}`);
|
||||
}
|
||||
if (userInfo.roles && userInfo.roles.length > 0) {
|
||||
console.log(`角色: ${userInfo.roles.join(', ')}`);
|
||||
}
|
||||
if (userInfo.permissions && userInfo.permissions.length > 0) {
|
||||
console.log(`权限: ${userInfo.permissions.join(', ')}`);
|
||||
}
|
||||
if (userInfo.createTime) {
|
||||
console.log(`创建时间: ${userInfo.createTime}`);
|
||||
}
|
||||
if (userInfo.loginDate) {
|
||||
console.log(`最后登录: ${userInfo.loginDate}`);
|
||||
}
|
||||
|
||||
// 打印会员信息 - 从 allMemberships 中获取最高等级
|
||||
if (membershipInfo && membershipInfo.allMemberships) {
|
||||
const highestTier = getHighestTierMembership(membershipInfo.allMemberships);
|
||||
|
||||
if (highestTier) {
|
||||
console.log('');
|
||||
console.log('会员信息:');
|
||||
console.log(`会员等级: ${highestTier.tierName} (${highestTier.tierCode})`);
|
||||
console.log(`等级层级: ${highestTier.tierLevel}`);
|
||||
console.log(`剩余天数: ${highestTier.remainingDays === -1 ? '永久' : highestTier.remainingDays + '天'}`);
|
||||
console.log(`月度积分: ${highestTier.monthlyCredits}`);
|
||||
|
||||
// 将最高等级会员信息合并到用户信息中
|
||||
userInfo.membership = {
|
||||
tierCode: highestTier.tierCode,
|
||||
tierName: highestTier.tierName,
|
||||
tierLevel: highestTier.tierLevel,
|
||||
remainingDays: highestTier.remainingDays,
|
||||
monthlyCredits: highestTier.monthlyCredits
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 保存到持久化存储
|
||||
await saveUserInfo(userInfo);
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('[UserService] 获取用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 持久化存储 ==============
|
||||
|
||||
let extensionContext: vscode.ExtensionContext | null = null;
|
||||
|
||||
/**
|
||||
* 初始化用户服务(设置 context)
|
||||
*/
|
||||
export function initUserService(context: vscode.ExtensionContext): void {
|
||||
extensionContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息到持久化存储
|
||||
*/
|
||||
export async function saveUserInfo(userInfo: UserInfo): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', userInfo);
|
||||
console.log('[UserService] 用户信息已保存到持久化存储');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从持久化存储获取用户信息
|
||||
*/
|
||||
export function getCachedUserInfo(): UserInfo | null {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return null;
|
||||
}
|
||||
return extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除持久化存储的用户信息
|
||||
*/
|
||||
export async function clearUserInfo(): Promise<void> {
|
||||
if (!extensionContext) {
|
||||
console.warn('[UserService] ExtensionContext 未初始化');
|
||||
return;
|
||||
}
|
||||
await extensionContext.globalState.update('icCoderUserInfo', undefined);
|
||||
console.log('[UserService] 用户信息已清除');
|
||||
}
|
||||
500
src/services/vcdFileServer.ts
Normal file
500
src/services/vcdFileServer.ts
Normal file
@ -0,0 +1,500 @@
|
||||
import * as http from "http";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/**
|
||||
* VCD 文件 HTTP 服务器
|
||||
* 用于为 Surfer 波形查看器提供 VCD 文件访问
|
||||
*/
|
||||
export class VCDFileServer {
|
||||
private server: http.Server | null = null;
|
||||
private port: number = 0;
|
||||
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
||||
private extensionUri: vscode.Uri;
|
||||
|
||||
constructor(extensionUri: vscode.Uri) {
|
||||
this.extensionUri = extensionUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
*/
|
||||
public async start(): Promise<number> {
|
||||
if (this.server) {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = http.createServer((req, res) => {
|
||||
this.handleRequest(req, res);
|
||||
});
|
||||
|
||||
// 监听随机端口
|
||||
this.server.listen(0, "127.0.0.1", () => {
|
||||
const address = this.server!.address();
|
||||
if (address && typeof address === "object") {
|
||||
this.port = address.port;
|
||||
console.log(`[VCDFileServer] 服务器已启动,端口: ${this.port}`);
|
||||
resolve(this.port);
|
||||
} else {
|
||||
reject(new Error("无法获取服务器端口"));
|
||||
}
|
||||
});
|
||||
|
||||
this.server.on("error", (error) => {
|
||||
console.error("[VCDFileServer] 服务器错误:", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务器
|
||||
*/
|
||||
public stop(): void {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
this.port = 0;
|
||||
this.vcdFiles.clear();
|
||||
console.log("[VCDFileServer] 服务器已停止");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 VCD 文件
|
||||
*/
|
||||
public registerFile(filePath: string): string {
|
||||
const fileId = this.generateFileId(filePath);
|
||||
this.vcdFiles.set(fileId, filePath);
|
||||
console.log(`[VCDFileServer] 注册文件: ${fileId} -> ${filePath}`);
|
||||
return fileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件 URL
|
||||
*/
|
||||
public getFileUrl(fileId: string): string {
|
||||
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取波形查看器 URL
|
||||
*/
|
||||
public getViewerUrl(fileId: string): string {
|
||||
return `http://127.0.0.1:${this.port}/viewer/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件 ID
|
||||
*/
|
||||
private generateFileId(filePath: string): string {
|
||||
const timestamp = Date.now();
|
||||
const fileName = path.basename(filePath);
|
||||
return `${timestamp}-${fileName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 请求
|
||||
*/
|
||||
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const url = req.url || "";
|
||||
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
||||
|
||||
// 设置 CORS 头
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
// 处理 OPTIONS 请求
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// 路由处理
|
||||
if (url.startsWith("/viewer/")) {
|
||||
this.handleViewerRequest(url, res);
|
||||
} else if (url.startsWith("/vcd/")) {
|
||||
this.handleVcdFileRequest(url, res);
|
||||
} else if (url.startsWith("/static/")) {
|
||||
this.handleStaticFileRequest(url, res);
|
||||
} else {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查看器页面请求
|
||||
*/
|
||||
private handleViewerRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/viewer\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = match[1];
|
||||
const filePath = this.vcdFiles.get(fileId);
|
||||
|
||||
if (!filePath) {
|
||||
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成 HTML 页面
|
||||
const html = this.generateViewerHtml(fileId, filePath);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Content-Length": Buffer.byteLength(html),
|
||||
});
|
||||
res.end(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 VCD 文件请求
|
||||
*/
|
||||
private handleVcdFileRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/vcd\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = match[1];
|
||||
const filePath = this.vcdFiles.get(fileId);
|
||||
|
||||
if (!filePath) {
|
||||
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[VCDFileServer] 文件不存在: ${filePath}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取并发送文件
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Length": fileContent.length,
|
||||
});
|
||||
res.end(fileContent);
|
||||
console.log(`[VCDFileServer] 成功发送文件: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error(`[VCDFileServer] 读取文件失败:`, error);
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理静态文件请求(Surfer 资源)
|
||||
*/
|
||||
private handleStaticFileRequest(url: string, res: http.ServerResponse): void {
|
||||
const match = url.match(/^\/static\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = match[1];
|
||||
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const contentType = this.getContentType(fileName);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": fileContent.length,
|
||||
});
|
||||
res.end(fileContent);
|
||||
} catch (error) {
|
||||
console.error(`[VCDFileServer] 读取静态文件失败:`, error);
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 Content-Type
|
||||
*/
|
||||
private getContentType(fileName: string): string {
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
const contentTypes: { [key: string]: string } = {
|
||||
".js": "application/javascript",
|
||||
".wasm": "application/wasm",
|
||||
".html": "text/html",
|
||||
".css": "text/css",
|
||||
};
|
||||
return contentTypes[ext] || "application/octet-stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||||
*/
|
||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||
try {
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||
const lines = buffer.split('\n');
|
||||
|
||||
const scopeNames: string[] = [];
|
||||
let scopeDepth = 0;
|
||||
const scopeStack: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith('$enddefinitions')) {
|
||||
break;
|
||||
}
|
||||
|
||||
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||||
if (scopeMatch) {
|
||||
const scopeType = scopeMatch[1];
|
||||
const scopeName = scopeMatch[2];
|
||||
|
||||
if (scopeDepth === 0 && scopeType === 'module') {
|
||||
scopeStack.push(scopeName);
|
||||
} else if (scopeDepth === 1 && scopeType === 'module') {
|
||||
const fullPath = [...scopeStack, scopeName];
|
||||
scopeNames.push(fullPath.join('.'));
|
||||
}
|
||||
|
||||
scopeDepth++;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('$upscope')) {
|
||||
scopeDepth--;
|
||||
if (scopeDepth === 0) {
|
||||
scopeStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopeNames;
|
||||
} catch (error) {
|
||||
console.error("[VCDFileServer] 解析 VCD 文件失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成波形查看器 HTML 页面
|
||||
*/
|
||||
private generateViewerHtml(fileId: string, vcdFilePath: string): string {
|
||||
const vcdUrl = this.getFileUrl(fileId);
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||||
const scopeNamesJson = JSON.stringify(scopeNames);
|
||||
|
||||
const htmlPart1 = this.getHtmlPart1(fileName);
|
||||
const htmlPart2 = this.getHtmlPart2(vcdUrl, scopeNamesJson);
|
||||
const htmlPart3 = this.getHtmlPart3();
|
||||
|
||||
return htmlPart1 + htmlPart2 + htmlPart3;
|
||||
}
|
||||
|
||||
private getHtmlPart1(fileName: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Surfer 波形查看器 - ${fileName}</title>
|
||||
<script>
|
||||
window.surferReady = false;
|
||||
window.pendingVcdData = null;
|
||||
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Surfer error:", msg);
|
||||
document.getElementById("error_message").innerHTML = msg;
|
||||
document.getElementById("error_container").style.display = "block";
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
</script>
|
||||
<script type="module">
|
||||
console.log('[Browser] 开始初始化 Surfer...');
|
||||
import init from '/static/surfer.js';
|
||||
await init({module_or_path: '/static/surfer_bg.wasm'});
|
||||
console.log('[Browser] Surfer WASM 已加载');
|
||||
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/static/surfer.js';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
window.surferReady = true;
|
||||
console.log('[Browser] Surfer 已完全初始化并准备就绪');
|
||||
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ToggleLogs"));
|
||||
console.log('[Browser] 已发送关闭日志面板命令');
|
||||
} catch (e) {
|
||||
console.log('[Browser] 关闭日志面板失败:', e);
|
||||
}
|
||||
|
||||
if (window.pendingVcdData) {
|
||||
console.log('[Browser] 发现待处理的 VCD 数据,立即加载');
|
||||
loadVcdUrl(window.pendingVcdData);
|
||||
window.pendingVcdData = null;
|
||||
}
|
||||
</script>`;
|
||||
}
|
||||
|
||||
private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string {
|
||||
return `
|
||||
<script>
|
||||
function loadVcdUrl(data) {
|
||||
try {
|
||||
console.log('[Browser] ========== 开始加载 VCD URL ==========');
|
||||
console.log('[Browser] URL:', data.url);
|
||||
console.log('[Browser] Scope names from VCD:', data.scopeNames);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('[Browser] 通过 postMessage 发送 LoadUrl 命令');
|
||||
window.postMessage({
|
||||
command: 'LoadUrl',
|
||||
url: data.url
|
||||
}, '*');
|
||||
console.log('[Browser] ✅ 已发送 LoadUrl 命令');
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('[Browser] 尝试自动添加所有信号');
|
||||
let scopeNamesToTry = [];
|
||||
|
||||
if (data.scopeNames && data.scopeNames.length > 0) {
|
||||
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||||
console.log('[Browser] 使用解析的作用域名称:', scopeNamesToTry);
|
||||
} else {
|
||||
scopeNamesToTry = [['top'], ['testbench'], ['tb'], ['test'], ['dut']];
|
||||
console.log('[Browser] 使用回退作用域名称');
|
||||
}
|
||||
|
||||
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||||
const scopeName = scopeNamesToTry[i];
|
||||
try {
|
||||
const addScopeMsg = {
|
||||
"AddScope": [
|
||||
{"strs": scopeName, "id": {"Wellen": i + 1}},
|
||||
true
|
||||
]
|
||||
};
|
||||
window.inject_message(JSON.stringify(addScopeMsg));
|
||||
console.log('[Browser] 已发送 AddScope: ' + scopeName.join('.'));
|
||||
} catch (e) {
|
||||
console.log('[Browser] AddScope 失败: ' + scopeName.join('.'), e);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ZoomToFit"));
|
||||
console.log('[Browser] 已发送 ZoomToFit 命令');
|
||||
} catch (e) {
|
||||
console.log('[Browser] ZoomToFit 失败:', e);
|
||||
}
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
console.error('[Browser] 添加信号失败:', e);
|
||||
}
|
||||
}, 1500);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('[Browser] ❌ 加载 VCD 失败:', error);
|
||||
on_surfer_error(error.message + '\\n' + error.stack);
|
||||
}
|
||||
}
|
||||
window.loadVcdUrl = loadVcdUrl;
|
||||
|
||||
// 页面加载完成后自动加载 VCD
|
||||
window.addEventListener('load', () => {
|
||||
const vcdData = {
|
||||
url: '${vcdUrl}',
|
||||
scopeNames: ${scopeNamesJson}
|
||||
};
|
||||
if (window.surferReady) {
|
||||
loadVcdUrl(vcdData);
|
||||
} else {
|
||||
window.pendingVcdData = vcdData;
|
||||
}
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
private getHtmlPart3(): string {
|
||||
return `
|
||||
<style>
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: #f48771;
|
||||
background-color: #5a1d1d;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
<div id="error_container" style="display: none;">
|
||||
<h3>❌ Surfer 加载失败</h3>
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
<script src="/static/integration.js"></script>
|
||||
<script>
|
||||
register_message_listener();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
172
src/types/api.ts
172
src/types/api.ts
@ -3,7 +3,7 @@
|
||||
* 对应后端 IC Coder Backend 的接口格式
|
||||
*/
|
||||
|
||||
import { CompactedMemory, CompactedMessage } from './memory';
|
||||
import { CompactedMemory, CompactedMessage } from "./memory";
|
||||
|
||||
// ============== 对话请求/响应 ==============
|
||||
|
||||
@ -14,7 +14,16 @@ import { CompactedMemory, CompactedMessage } from './memory';
|
||||
* - agent: 智能体自主(默认)
|
||||
* - auto: 完全自动
|
||||
*/
|
||||
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
||||
export type RunMode = "plan" | "ask" | "agent" | "auto";
|
||||
|
||||
/**
|
||||
* 服务等级类型
|
||||
* - lite: 轻量级
|
||||
* - syntaxic: 语法级
|
||||
* - max: 最大性能
|
||||
* - auto: 自动选择
|
||||
*/
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
|
||||
/**
|
||||
* 对话请求
|
||||
@ -29,6 +38,8 @@ export interface DialogRequest {
|
||||
userId: string;
|
||||
/** 运行模式 */
|
||||
mode: RunMode;
|
||||
/** 服务等级 */
|
||||
serviceTier?: ServiceTier;
|
||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||
compactedData?: CompactedMemory;
|
||||
/** 压缩后产生的新消息 */
|
||||
@ -41,25 +52,26 @@ export interface DialogRequest {
|
||||
|
||||
/** SSE 事件类型枚举 */
|
||||
export type SSEEventType =
|
||||
| 'text_delta' // 文本增量
|
||||
| 'tool_call' // 客户端工具调用请求
|
||||
| 'tool_confirm' // 工具确认请求(Ask 模式)
|
||||
| 'plan_confirm' // 计划确认请求(Plan 模式)
|
||||
| 'tool_start' // 工具开始执行
|
||||
| 'tool_complete' // 工具执行完成
|
||||
| 'tool_error' // 工具执行错误
|
||||
| 'ask_user' // 向用户提问
|
||||
| 'agent_start' // 子智能体启动
|
||||
| 'agent_progress' // 子智能体进度
|
||||
| 'agent_complete' // 子智能体完成
|
||||
| 'agent_error' // 子智能体错误
|
||||
| 'memory_compacted' // 记忆压缩完成
|
||||
| 'context_usage' // 上下文使用量
|
||||
| 'complete' // 对话完成
|
||||
| 'error' // 错误
|
||||
| 'warning' // 警告
|
||||
| 'notification' // 通知
|
||||
| 'depth_update'; // 深度更新
|
||||
| "text_delta" // 文本增量
|
||||
| "tool_call" // 客户端工具调用请求
|
||||
| "tool_confirm" // 工具确认请求(Ask 模式)
|
||||
| "plan_confirm" // 计划确认请求(Plan 模式)
|
||||
| "tool_start" // 工具开始执行
|
||||
| "tool_complete" // 工具执行完成
|
||||
| "tool_error" // 工具执行错误
|
||||
| "ask_user" // 向用户提问
|
||||
| "agent_start" // 子智能体启动
|
||||
| "agent_progress" // 子智能体进度
|
||||
| "agent_complete" // 子智能体完成
|
||||
| "agent_error" // 子智能体错误
|
||||
| "memory_compacted" // 记忆压缩完成
|
||||
| "context_usage" // 上下文使用量
|
||||
| "complete" // 对话完成
|
||||
| "error" // 错误
|
||||
| "warning" // 警告
|
||||
| "notification" // 通知
|
||||
| "depth_update" // 深度更新
|
||||
| "heartbeat"; // 心跳
|
||||
|
||||
/** text_delta 事件数据 */
|
||||
export interface TextDeltaEvent {
|
||||
@ -161,7 +173,7 @@ export interface AgentProgressEvent {
|
||||
toolName: string;
|
||||
toolInput?: unknown;
|
||||
toolResult?: string;
|
||||
status: 'running' | 'completed' | 'error';
|
||||
status: "running" | "completed" | "error";
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@ -197,11 +209,11 @@ export interface ContextUsageEvent {
|
||||
*/
|
||||
export interface ToolCallRequest {
|
||||
/** JSON-RPC版本,固定为"2.0" */
|
||||
jsonrpc: '2.0';
|
||||
jsonrpc: "2.0";
|
||||
/** 请求ID,用于匹配响应 */
|
||||
id: number;
|
||||
/** 方法名,固定为"tools/call" */
|
||||
method: 'tools/call';
|
||||
method: "tools/call";
|
||||
/** 调用参数 */
|
||||
params: {
|
||||
/** 工具名称 */
|
||||
@ -217,7 +229,7 @@ export interface ToolCallRequest {
|
||||
*/
|
||||
export interface ToolCallResult {
|
||||
/** JSON-RPC版本 */
|
||||
jsonrpc: '2.0';
|
||||
jsonrpc: "2.0";
|
||||
/** 请求ID,与ToolCallRequest.id对应 */
|
||||
id: number;
|
||||
/** 执行结果(与error互斥) */
|
||||
@ -298,20 +310,110 @@ export interface ToolConfirmResponse {
|
||||
approved: boolean;
|
||||
}
|
||||
|
||||
// ============== 用户信息 ==============
|
||||
|
||||
/**
|
||||
* 用户信息响应
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export interface UserInfoResponse {
|
||||
/** 响应消息 */
|
||||
msg: string;
|
||||
/** 响应代码 (200 表示成功) */
|
||||
code: number;
|
||||
/** 权限列表 */
|
||||
permissions: string[];
|
||||
/** 角色列表 */
|
||||
roles: string[];
|
||||
/** 是否默认修改密码 */
|
||||
isDefaultModifyPwd: boolean;
|
||||
/** 密码是否过期 */
|
||||
isPasswordExpired: boolean;
|
||||
/** 用户信息 */
|
||||
user: {
|
||||
userId: number;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
email?: string;
|
||||
phonenumber?: string;
|
||||
sex?: string;
|
||||
avatar?: string;
|
||||
status?: string;
|
||||
createTime?: string;
|
||||
loginDate?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
// ============== 会员信息 ==============
|
||||
|
||||
/**
|
||||
* 会员单条记录
|
||||
*/
|
||||
export interface MembershipItemVO {
|
||||
membershipId: number | null;
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
expireTime: string | null;
|
||||
remainingDays: number;
|
||||
permanent: boolean;
|
||||
nextGrantTime: string | null;
|
||||
lastGrantTime: string | null;
|
||||
grantCycle: number;
|
||||
totalGranted: number;
|
||||
monthlyCredits: number;
|
||||
teamSeat: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户会员信息
|
||||
*/
|
||||
export interface UserMembershipVO {
|
||||
userId: number;
|
||||
tierCode: string;
|
||||
tierName: string;
|
||||
tierLevel: number;
|
||||
allowedModelCombinations: string[];
|
||||
description?: string;
|
||||
createdTime?: string;
|
||||
updatedTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多会员信息响应
|
||||
*/
|
||||
export interface MultiMembershipVO extends UserMembershipVO {
|
||||
displayTier?: MembershipItemVO;
|
||||
allMemberships?: MembershipItemVO[];
|
||||
totalMonthlyCredits?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员信息响应
|
||||
* GET /strangeloop/api/membership/current
|
||||
*/
|
||||
export interface MembershipResponse {
|
||||
code: number;
|
||||
msg?: string;
|
||||
message?: string;
|
||||
data?: MultiMembershipVO;
|
||||
}
|
||||
|
||||
// ============== 辅助类型 ==============
|
||||
|
||||
/** 后端工具名称 */
|
||||
export type ToolName =
|
||||
| 'file_read'
|
||||
| 'file_write'
|
||||
| 'file_delete'
|
||||
| 'file_list'
|
||||
| 'syntax_check'
|
||||
| 'simulation'
|
||||
| 'waveform_summary'
|
||||
| 'waveform_trace'
|
||||
| 'knowledge_save'
|
||||
| 'knowledge_load';
|
||||
| "file_read"
|
||||
| "file_write"
|
||||
| "file_delete"
|
||||
| "file_list"
|
||||
| "syntax_check"
|
||||
| "simulation"
|
||||
| "waveform_summary"
|
||||
| "waveform_trace"
|
||||
| "knowledge_save"
|
||||
| "knowledge_load";
|
||||
|
||||
/** file_read 工具参数 */
|
||||
export interface FileReadArgs {
|
||||
|
||||
@ -19,7 +19,7 @@ import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
|
||||
import type { RunMode } from "../types/api";
|
||||
import type { RunMode, ServiceTier } from "../types/api";
|
||||
|
||||
/** 是否使用后端服务(可通过配置控制) */
|
||||
let useBackendService = true;
|
||||
@ -58,7 +58,8 @@ export async function handleUserMessage(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath?: string,
|
||||
mode?: RunMode
|
||||
mode?: RunMode,
|
||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
@ -90,7 +91,7 @@ export async function handleUserMessage(
|
||||
// 尝试使用后端服务
|
||||
if (useBackendService && extensionPath) {
|
||||
try {
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用:", error);
|
||||
@ -125,7 +126,8 @@ async function handleUserMessageWithBackend(
|
||||
text: string,
|
||||
extensionPath: string,
|
||||
mode?: RunMode,
|
||||
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||
): Promise<void> {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
@ -287,7 +289,8 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
},
|
||||
},
|
||||
mode
|
||||
mode,
|
||||
serviceTier // 传递服务等级
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -160,13 +160,17 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
|
||||
// 处理侧边栏的消息
|
||||
webviewView.webview.onDidReceiveMessage((message) => {
|
||||
webviewView.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
if (message.command === "openChat") {
|
||||
vscode.commands.executeCommand("ic-coder.openChat");
|
||||
} else if (message.command === "login") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
this.context.subscriptions
|
||||
);
|
||||
}
|
||||
|
||||
private getWebviewContent(
|
||||
|
||||
@ -96,6 +96,27 @@ export function getAgentCardStyles(): string {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.agent-step.low-profile {
|
||||
opacity: 0.85;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-step.low-profile .step-icon {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.agent-step.low-profile .step-name {
|
||||
font-weight: 400;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.9;
|
||||
}
|
||||
.agent-step.low-profile .step-result {
|
||||
opacity: 0.85;
|
||||
font-size: 11px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -119,8 +140,8 @@ export function getAgentCardScript(): string {
|
||||
'queryKnowledgeSummary': '查询知识摘要',
|
||||
'queryRules': '查询规则',
|
||||
'setModule': '设置模块',
|
||||
'addSignal': '添加信号',
|
||||
'addSignalExample': '添加信号示例',
|
||||
'addSignal': '正在分析信号定义',
|
||||
'addSignalExample': '正在处理信号示例',
|
||||
'validateKnowledgeGraph': '验证知识图谱',
|
||||
'querySignals': '查询信号',
|
||||
'addPlan': '添加计划',
|
||||
@ -151,7 +172,9 @@ export function getAgentCardScript(): string {
|
||||
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
||||
const displayName = getAgentToolDisplayName(step.toolName);
|
||||
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
|
||||
return \`<div class="agent-step"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
|
||||
// 所有工具调用都使用低调样式
|
||||
const stepClass = 'agent-step low-profile';
|
||||
return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
|
||||
@ -8,6 +8,12 @@
|
||||
* - Agent: 智能体自主,自动执行大部分操作
|
||||
*/
|
||||
|
||||
import {
|
||||
plannerIconSvg,
|
||||
askIconSvg,
|
||||
agentIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取模式选择器的 HTML 内容
|
||||
*/
|
||||
@ -23,16 +29,25 @@ export function getModeSelectorContent(): string {
|
||||
</div>
|
||||
<div class="mode-dropdown" id="modeDropdown">
|
||||
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
|
||||
<div class="mode-option-header">
|
||||
<span class="mode-option-icon">${plannerIconSvg}</span>
|
||||
<span class="mode-option-label">Plan</span>
|
||||
<span class="mode-option-desc">只读模式</span>
|
||||
</div>
|
||||
<span class="mode-option-desc">仅根据需求生成设计文档,之后由用户决定下一步,可以提高工程质量</span>
|
||||
</div>
|
||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
|
||||
<div class="mode-option-header">
|
||||
<span class="mode-option-icon">${askIconSvg}</span>
|
||||
<span class="mode-option-label">Ask</span>
|
||||
<span class="mode-option-desc">逐个确认</span>
|
||||
</div>
|
||||
<span class="mode-option-desc">仅给与智能体读权限,用于依据项目回答用户问题,或者与用户进行探讨</span>
|
||||
</div>
|
||||
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
|
||||
<div class="mode-option-header">
|
||||
<span class="mode-option-icon">${agentIconSvg}</span>
|
||||
<span class="mode-option-label">Agent</span>
|
||||
<span class="mode-option-desc">智能体自主</span>
|
||||
</div>
|
||||
<span class="mode-option-desc">用于快速生成工程、调试修改现有代码</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -83,7 +98,8 @@ export function getModeSelectorStyles(): string {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 0;
|
||||
min-width: 140px;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 4px;
|
||||
@ -98,13 +114,12 @@ export function getModeSelectorStyles(): string {
|
||||
/* 模式选择器的选项样式 */
|
||||
.mode-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mode-option:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
@ -112,13 +127,31 @@ export function getModeSelectorStyles(): string {
|
||||
.mode-option.selected {
|
||||
background: rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
.mode-option-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.mode-option-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mode-option-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
.mode-option-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.mode-option-desc {
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 12px;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -157,9 +190,9 @@ export function getModeSelectorScript(): string {
|
||||
// 更新 tooltip
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': '只读模式 - 只能查询分析',
|
||||
'ask': '逐个确认 - 每个写操作需确认',
|
||||
'agent': '智能体自主模式','
|
||||
'plan': 'plan模式',
|
||||
'ask': 'ask模式',
|
||||
'agent': 'agent模式'
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
|
||||
@ -7,14 +7,78 @@
|
||||
*/
|
||||
export function getContextButtonContent(): string {
|
||||
return `
|
||||
<div class="context-selector-wrapper">
|
||||
<div class="tooltip">
|
||||
<button class="add-context-button" onclick="handleAddContext()">
|
||||
<button class="add-context-button" onclick="toggleContextMenu()">
|
||||
<svg t="1766915545722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4994" width="200" height="200">
|
||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
||||
</svg>
|
||||
<span class="add-context-label">添加上下文</span>
|
||||
<svg class="dropdown-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 714.666667L213.333333 416l42.666667-42.666667L512 629.333333l256-256 42.666667 42.666667z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="tooltiptext">添加文件或代码片段作为上下文</span>
|
||||
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
||||
</div>
|
||||
|
||||
<!-- 上拉菜单 -->
|
||||
<div class="context-menu" id="contextMenu">
|
||||
<!-- 主菜单 -->
|
||||
<div class="context-menu-main" id="contextMenuMain">
|
||||
<div class="context-menu-item" onclick="showFileList()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326z m1.8 562H232V136h302v216c0 23.2 18.8 42 42 42h216v494z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文件</span>
|
||||
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="showFolderList()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文件夹</span>
|
||||
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="handleAddImage()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>图片</span>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="handleAddDocument()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>文档库</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件/文件夹列表视图 -->
|
||||
<div class="context-menu-list" id="contextMenuList" style="display: none;">
|
||||
<div class="context-menu-list-header">
|
||||
<button class="context-menu-back" onclick="backToMainMenu()">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8c-16.4 12.8-16.4 37.5 0 50.3l450.8 352.1c5.3 4.1 12.9 0.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="contextMenuListTitle">选择文件</span>
|
||||
</div>
|
||||
<div class="context-menu-list-body" id="contextMenuListBody">
|
||||
<!-- 动态加载列表 -->
|
||||
</div>
|
||||
<div class="context-menu-list-footer">
|
||||
<input type="text" id="contextMenuSearch" placeholder="搜索..." />
|
||||
<div class="context-menu-list-actions">
|
||||
<span id="contextMenuListCount">已选择 0 项</span>
|
||||
<button class="primary" onclick="confirmSelection()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -24,6 +88,12 @@ export function getContextButtonContent(): string {
|
||||
*/
|
||||
export function getContextButtonStyles(): string {
|
||||
return `
|
||||
/* 上下文选择器容器 */
|
||||
.context-selector-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 添加上下文按钮样式 */
|
||||
.add-context-button {
|
||||
display: flex;
|
||||
@ -45,15 +115,218 @@ export function getContextButtonStyles(): string {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.add-context-button svg {
|
||||
.add-context-button svg.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.add-context-button .dropdown-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.add-context-button.active .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.add-context-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 上拉菜单样式 */
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 0;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
min-width: 180px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context-menu.show {
|
||||
display: block;
|
||||
animation: slideUp 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.context-menu-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.context-menu-item span {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.context-menu-item .arrow-right {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.6;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 列表视图样式 */
|
||||
.context-menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.context-menu-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.context-menu-back {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.context-menu-back:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.context-menu-back svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.context-menu-list-header span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.context-menu-list-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.context-menu-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.context-menu-list-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.context-menu-list-item.selected {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
.context-menu-list-item input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-menu-list-item label {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-menu-list-footer {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.context-menu-list-footer input {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--vscode-input-foreground);
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.context-menu-list-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.context-menu-list-footer span {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.context-menu-list-footer button {
|
||||
padding: 4px 12px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.context-menu-list-footer button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -62,10 +335,174 @@ export function getContextButtonStyles(): string {
|
||||
*/
|
||||
export function getContextButtonScript(): string {
|
||||
return `
|
||||
// 添加上下文处理函数
|
||||
function handleAddContext() {
|
||||
// 发送添加上下文请求到扩展
|
||||
vscode.postMessage({ command: 'addContext' });
|
||||
// 上下文菜单状态
|
||||
let currentListData = [];
|
||||
let currentListType = '';
|
||||
let selectedItems = new Set();
|
||||
|
||||
// 切换上下文菜单显示/隐藏
|
||||
function toggleContextMenu() {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const button = document.querySelector('.add-context-button');
|
||||
|
||||
if (menu && button) {
|
||||
const isShown = menu.classList.contains('show');
|
||||
|
||||
if (isShown) {
|
||||
menu.classList.remove('show');
|
||||
button.classList.remove('active');
|
||||
backToMainMenu(); // 关闭时回到主菜单
|
||||
} else {
|
||||
menu.classList.add('show');
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭菜单
|
||||
document.addEventListener('click', function(event) {
|
||||
const wrapper = document.querySelector('.context-selector-wrapper');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const button = document.querySelector('.add-context-button');
|
||||
|
||||
if (wrapper && menu && button && !wrapper.contains(event.target)) {
|
||||
menu.classList.remove('show');
|
||||
button.classList.remove('active');
|
||||
backToMainMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// 显示文件列表
|
||||
function showFileList() {
|
||||
vscode.postMessage({ command: 'addContextFile' });
|
||||
}
|
||||
|
||||
// 显示文件夹列表
|
||||
function showFolderList() {
|
||||
vscode.postMessage({ command: 'addContextFolder' });
|
||||
}
|
||||
|
||||
// 返回主菜单
|
||||
function backToMainMenu() {
|
||||
const mainMenu = document.getElementById('contextMenuMain');
|
||||
const listView = document.getElementById('contextMenuList');
|
||||
|
||||
if (mainMenu && listView) {
|
||||
mainMenu.style.display = 'block';
|
||||
listView.style.display = 'none';
|
||||
}
|
||||
|
||||
selectedItems.clear();
|
||||
currentListData = [];
|
||||
}
|
||||
|
||||
// 切换到列表视图
|
||||
function switchToListView(title, type, data) {
|
||||
const mainMenu = document.getElementById('contextMenuMain');
|
||||
const listView = document.getElementById('contextMenuList');
|
||||
const titleEl = document.getElementById('contextMenuListTitle');
|
||||
|
||||
if (mainMenu && listView && titleEl) {
|
||||
mainMenu.style.display = 'none';
|
||||
listView.style.display = 'flex';
|
||||
titleEl.textContent = title;
|
||||
|
||||
currentListType = type;
|
||||
currentListData = data;
|
||||
selectedItems.clear();
|
||||
|
||||
renderList(data);
|
||||
updateSelectedCount();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染列表
|
||||
function renderList(data) {
|
||||
const body = document.getElementById('contextMenuListBody');
|
||||
if (!body) return;
|
||||
|
||||
body.innerHTML = data.map((item, index) => \`
|
||||
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
|
||||
<input type="checkbox" id="item-\${index}" />
|
||||
<label for="item-\${index}">\${item.relativePath}</label>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 切换项选择
|
||||
function toggleItemSelection(index) {
|
||||
const checkbox = document.getElementById('item-' + index);
|
||||
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
||||
|
||||
if (checkbox && item) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
||||
if (checkbox.checked) {
|
||||
selectedItems.add(index);
|
||||
item.classList.add('selected');
|
||||
} else {
|
||||
selectedItems.delete(index);
|
||||
item.classList.remove('selected');
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选中数量
|
||||
function updateSelectedCount() {
|
||||
const countEl = document.getElementById('contextMenuListCount');
|
||||
if (countEl) {
|
||||
countEl.textContent = '已选择 ' + selectedItems.size + ' 项';
|
||||
}
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
function confirmSelection() {
|
||||
const selected = Array.from(selectedItems).map(index => currentListData[index]);
|
||||
|
||||
if (selected.length > 0) {
|
||||
selected.forEach(item => {
|
||||
addContextItem(currentListType, item.path);
|
||||
});
|
||||
}
|
||||
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 添加图片
|
||||
function handleAddImage() {
|
||||
vscode.postMessage({ command: 'addContextImage' });
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 添加文档
|
||||
function handleAddDocument() {
|
||||
vscode.postMessage({ command: 'addContextDocument' });
|
||||
toggleContextMenu();
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const searchInput = document.getElementById('contextMenuSearch');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const keyword = e.target.value.toLowerCase();
|
||||
const filtered = currentListData.filter(item =>
|
||||
item.relativePath.toLowerCase().includes(keyword)
|
||||
);
|
||||
renderList(filtered);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理后端消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
if (message.command === 'showWorkspaceFileList') {
|
||||
switchToListView('选择文件', 'file', message.files);
|
||||
} else if (message.command === 'showWorkspaceFolderList') {
|
||||
switchToListView('选择文件夹', 'folder', message.folders);
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
225
src/views/contextDisplay.ts
Normal file
225
src/views/contextDisplay.ts
Normal file
@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 上下文显示组件
|
||||
* 用于显示已选择的文件、文件夹、图片和文档
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取上下文显示区域的 HTML 内容
|
||||
*/
|
||||
export function getContextDisplayContent(): string {
|
||||
return `
|
||||
<div class="context-display-area" id="contextDisplayArea" style="display: none;">
|
||||
<div class="context-items-container" id="contextItemsContainer">
|
||||
<!-- 动态添加的上下文项将显示在这里 -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文显示区域的样式
|
||||
*/
|
||||
export function getContextDisplayStyles(): string {
|
||||
return `
|
||||
/* 上下文显示区域 */
|
||||
.context-display-area {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
|
||||
.context-items-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 上下文项样式 */
|
||||
.context-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
max-width: 300px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.context-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.context-item svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.context-item-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-item-remove {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-item-remove:hover {
|
||||
opacity: 1;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 图片预览样式 */
|
||||
.context-item.image-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.context-item-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文显示区域的脚本
|
||||
*/
|
||||
export function getContextDisplayScript(): string {
|
||||
return `
|
||||
// 存储上下文项
|
||||
let contextItems = [];
|
||||
|
||||
// 获取文件图标 SVG
|
||||
function getFileIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取文件夹图标 SVG
|
||||
function getFolderIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取图片图标 SVG
|
||||
function getImageIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取文档图标 SVG
|
||||
function getDocumentIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 获取删除图标 SVG
|
||||
function getRemoveIcon() {
|
||||
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
// 提取文件名
|
||||
function getFileName(path) {
|
||||
return path.split(/[\\\\/]/).pop();
|
||||
}
|
||||
|
||||
// 添加上下文项
|
||||
function addContextItem(type, path) {
|
||||
const id = Date.now() + Math.random();
|
||||
contextItems.push({ id, type, path });
|
||||
renderContextItems();
|
||||
}
|
||||
|
||||
// 删除上下文项
|
||||
function removeContextItem(id) {
|
||||
contextItems = contextItems.filter(item => item.id !== id);
|
||||
renderContextItems();
|
||||
}
|
||||
|
||||
// 渲染上下文项
|
||||
function renderContextItems() {
|
||||
const container = document.getElementById('contextItemsContainer');
|
||||
const displayArea = document.getElementById('contextDisplayArea');
|
||||
|
||||
if (!container || !displayArea) return;
|
||||
|
||||
if (contextItems.length === 0) {
|
||||
displayArea.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
displayArea.style.display = 'block';
|
||||
container.innerHTML = contextItems.map(item => {
|
||||
let icon = '';
|
||||
switch(item.type) {
|
||||
case 'file': icon = getFileIcon(); break;
|
||||
case 'folder': icon = getFolderIcon(); break;
|
||||
case 'image': icon = getImageIcon(); break;
|
||||
case 'document': icon = getDocumentIcon(); break;
|
||||
}
|
||||
|
||||
return \`
|
||||
<div class="context-item" title="\${item.path}">
|
||||
\${icon}
|
||||
<span class="context-item-name">\${getFileName(item.path)}</span>
|
||||
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
||||
\${getRemoveIcon()}
|
||||
</span>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 处理后端返回的文件选择结果
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
switch(message.command) {
|
||||
case 'contextFilesSelected':
|
||||
if (message.files && message.files.length > 0) {
|
||||
message.files.forEach(file => addContextItem('file', file));
|
||||
}
|
||||
break;
|
||||
case 'contextFoldersSelected':
|
||||
if (message.folders && message.folders.length > 0) {
|
||||
message.folders.forEach(folder => addContextItem('folder', folder));
|
||||
}
|
||||
break;
|
||||
case 'contextImagesSelected':
|
||||
if (message.images && message.images.length > 0) {
|
||||
message.images.forEach(image => addContextItem('image', image));
|
||||
}
|
||||
break;
|
||||
case 'contextDocumentsSelected':
|
||||
if (message.documents && message.documents.length > 0) {
|
||||
message.documents.forEach(doc => addContextItem('document', doc));
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有上下文项(供发送消息时使用)
|
||||
window.getContextItems = function() {
|
||||
return contextItems;
|
||||
};
|
||||
|
||||
// 清空上下文项(供清空对话时使用)
|
||||
window.clearContextItems = function() {
|
||||
contextItems = [];
|
||||
renderContextItems();
|
||||
};
|
||||
`;
|
||||
}
|
||||
@ -1,3 +1,10 @@
|
||||
import {
|
||||
getUserInfoComponentContent,
|
||||
getUserInfoComponentStyles,
|
||||
getUserInfoComponentScript,
|
||||
} from "./userInfoComponent";
|
||||
import { userAvatarIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 HTML 内容
|
||||
*/
|
||||
@ -6,7 +13,7 @@ export function getConversationHistoryBarContent(): string {
|
||||
<div class="conversation-history-bar">
|
||||
<div class="history-dropdown-container">
|
||||
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
|
||||
<span class="dropdown-label">Past Conversations</span>
|
||||
<span class="dropdown-label">历史对话</span>
|
||||
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
|
||||
</svg>
|
||||
@ -19,11 +26,20 @@ export function getConversationHistoryBarContent(): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="user-info-container">
|
||||
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
|
||||
${userAvatarIconSvg}
|
||||
</button>
|
||||
${getUserInfoComponentContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -49,13 +65,59 @@ export function getConversationHistoryBarStyles(): string {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-info-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar-icon-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar-icon-button:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button.active {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.user-avatar-icon-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
${getUserInfoComponentStyles()}
|
||||
|
||||
.history-dropdown-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-input-foreground);
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -64,7 +126,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.history-dropdown-button:hover {
|
||||
opacity: 0.8;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
@ -163,7 +225,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -173,11 +235,12 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
opacity: 0.7;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.new-conversation-button:active {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.new-conversation-button svg {
|
||||
@ -210,6 +273,29 @@ export function getConversationHistoryBarStyles(): string {
|
||||
*/
|
||||
export function getConversationHistoryBarScript(): string {
|
||||
return `
|
||||
${getUserInfoComponentScript()}
|
||||
|
||||
// 更新用户头像图标按钮显示
|
||||
function updateUserAvatarIconButton(userInfo) {
|
||||
const userAvatarIconButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (userInfo && userInfo.nickname) {
|
||||
// 显示用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'flex';
|
||||
}
|
||||
// 同时更新用户详情弹窗的数据
|
||||
if (typeof updateUserInfoDisplay === 'function') {
|
||||
updateUserInfoDisplay(userInfo);
|
||||
}
|
||||
} else {
|
||||
// 隐藏用户头像图标按钮
|
||||
if (userAvatarIconButton) {
|
||||
userAvatarIconButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会话历史相关变量
|
||||
let conversationHistory = [];
|
||||
let currentConversationId = null;
|
||||
|
||||
@ -14,6 +14,11 @@ import {
|
||||
getContextButtonStyles,
|
||||
getContextButtonScript,
|
||||
} from "./contextButton";
|
||||
import {
|
||||
getContextDisplayContent,
|
||||
getContextDisplayStyles,
|
||||
getContextDisplayScript,
|
||||
} from "./contextDisplay";
|
||||
import {
|
||||
getContextCompressContent,
|
||||
getContextCompressStyles,
|
||||
@ -36,13 +41,15 @@ export function getInputAreaContent(
|
||||
maxIcon: string = ''
|
||||
): string {
|
||||
return `
|
||||
<div class="input-area">
|
||||
<div class="input-area centered" id="inputArea">
|
||||
<div class="input-group">
|
||||
<div class="input-wrapper">
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="input-top-toolbar">
|
||||
${getContextButtonContent()}
|
||||
</div>
|
||||
<!-- 上下文显示区域 -->
|
||||
${getContextDisplayContent()}
|
||||
<textarea
|
||||
id="messageInput"
|
||||
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
||||
@ -76,12 +83,30 @@ export function getInputAreaStyles(): string {
|
||||
${getModeSelectorStyles()}
|
||||
${getModelSelectorStyles()}
|
||||
${getContextButtonStyles()}
|
||||
${getContextDisplayStyles()}
|
||||
${getContextCompressStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
.input-area {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
padding-top: 15px;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
/* 居中模式:未发起对话时 */
|
||||
.input-area.centered {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% - 40px);
|
||||
max-width: 800px;
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
/* 底部模式:发起对话后 */
|
||||
.input-area.bottom {
|
||||
position: relative;
|
||||
transform: none;
|
||||
}
|
||||
.input-group {
|
||||
display: flex;
|
||||
@ -264,16 +289,34 @@ export function getInputAreaScript(): string {
|
||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||
${getModelSelectorScript()}
|
||||
${getContextButtonScript()}
|
||||
${getContextDisplayScript()}
|
||||
${getContextCompressScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
let hasMessages = false; // 是否已有消息
|
||||
|
||||
// 工作区检测状态
|
||||
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||||
let hasWorkspace = true; // 工作区状态
|
||||
|
||||
// 切换输入框布局模式
|
||||
function updateInputAreaLayout() {
|
||||
const inputArea = document.getElementById('inputArea');
|
||||
if (!inputArea) return;
|
||||
|
||||
if (hasMessages) {
|
||||
// 有消息时,移到底部
|
||||
inputArea.classList.remove('centered');
|
||||
inputArea.classList.add('bottom');
|
||||
} else {
|
||||
// 无消息时,居中显示
|
||||
inputArea.classList.add('centered');
|
||||
inputArea.classList.remove('bottom');
|
||||
}
|
||||
}
|
||||
|
||||
// 自动调整 textarea 高度
|
||||
function autoResizeTextarea() {
|
||||
if (messageInput) {
|
||||
@ -357,12 +400,26 @@ export function getInputAreaScript(): string {
|
||||
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||||
const planMode = document.getElementById('planToggle')?.checked || false;
|
||||
|
||||
// 获取上下文项
|
||||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||
|
||||
addMessage(text, 'user');
|
||||
|
||||
// 标记已有消息,切换布局到底部
|
||||
hasMessages = true;
|
||||
updateInputAreaLayout();
|
||||
|
||||
// 切换按钮为暂停状态
|
||||
setSendButtonState(true);
|
||||
|
||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model, planMode: planMode });
|
||||
vscode.postMessage({
|
||||
command: 'sendMessage',
|
||||
text: text,
|
||||
mode: mode,
|
||||
model: model,
|
||||
planMode: planMode,
|
||||
contextItems: contextItems
|
||||
});
|
||||
messageInput.value = '';
|
||||
autoResizeTextarea(); // 重置输入框高度
|
||||
messageInput.focus();
|
||||
@ -370,5 +427,28 @@ export function getInputAreaScript(): string {
|
||||
// 重置优化状态
|
||||
resetOptimizeButton();
|
||||
}
|
||||
|
||||
// 全局函数:重置输入框布局(用于清空对话时)
|
||||
window.resetInputAreaLayout = function() {
|
||||
hasMessages = false;
|
||||
updateInputAreaLayout();
|
||||
};
|
||||
|
||||
// 全局函数:检查是否有消息(用于页面加载时)
|
||||
window.checkMessagesAndUpdateLayout = function() {
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
if (messagesContainer) {
|
||||
const messageElements = messagesContainer.querySelectorAll('.message');
|
||||
hasMessages = messageElements.length > 0;
|
||||
updateInputAreaLayout();
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时检查消息状态
|
||||
setTimeout(() => {
|
||||
if (window.checkMessagesAndUpdateLayout) {
|
||||
window.checkMessagesAndUpdateLayout();
|
||||
}
|
||||
}, 100);
|
||||
`;
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
waveformIconSvg,
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
import {
|
||||
getWaveformPreviewContent,
|
||||
@ -30,6 +31,10 @@ import {
|
||||
} from "./waveformPreviewContent";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
|
||||
import {
|
||||
getCodeHighlightStyles,
|
||||
getCodeHighlightScript,
|
||||
} from "../components/codeHighlight";
|
||||
|
||||
/**
|
||||
* 获取消息区域的 HTML 内容
|
||||
@ -294,7 +299,7 @@ export function getMessageAreaStyles(): string {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -303,76 +308,80 @@ export function getMessageAreaStyles(): string {
|
||||
/* Markdown 样式 */
|
||||
.segment-text h1,
|
||||
.segment-text h2,
|
||||
.segment-text h3 {
|
||||
margin: 16px 0 8px 0;
|
||||
.segment-text h3,
|
||||
.question-text h1,
|
||||
.question-text h2,
|
||||
.question-text h3 {
|
||||
margin: 0px 0 -10px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.segment-text h1 {
|
||||
.segment-text h1,
|
||||
.question-text h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.segment-text h2 {
|
||||
.segment-text h2,
|
||||
.question-text h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.segment-text h3 {
|
||||
.segment-text h3,
|
||||
.question-text h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.segment-text pre {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.segment-text code {
|
||||
font-family: 'Courier New', Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.segment-text pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.segment-text code:not(pre code) {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--vscode-textPreformat-foreground);
|
||||
}
|
||||
.segment-text ul,
|
||||
.segment-text ol {
|
||||
.segment-text ol,
|
||||
.question-text ul,
|
||||
.question-text ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.segment-text li {
|
||||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
.segment-text li,
|
||||
.question-text li {
|
||||
line-height: 1;
|
||||
}
|
||||
.segment-text strong {
|
||||
.segment-text strong,
|
||||
.question-text strong {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.segment-text em {
|
||||
.segment-text em,
|
||||
.question-text em {
|
||||
font-style: italic;
|
||||
}
|
||||
.segment-text a {
|
||||
.segment-text a,
|
||||
.question-text a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.segment-text a:hover {
|
||||
.segment-text a:hover,
|
||||
.question-text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.segment-text p {
|
||||
.segment-text p,
|
||||
.question-text p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.segment-text code,
|
||||
.question-text code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.segment-tool {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
/* 低调显示的工具调用 - 移除边距和背景 */
|
||||
.segment-tool.low-profile {
|
||||
margin: 2px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.tool-segment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -532,11 +541,28 @@ export function getMessageAreaStyles(): string {
|
||||
.tool-segment-content.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-icon {
|
||||
opacity: 0.55;
|
||||
font-size: 11px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-name {
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-result {
|
||||
opacity: 0.7;
|
||||
font-size: 10px;
|
||||
}
|
||||
.segment-question {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 14px;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.segment-question .question-text {
|
||||
@ -584,6 +610,7 @@ export function getMessageAreaStyles(): string {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.segment-question .custom-submit {
|
||||
padding: 8px 16px;
|
||||
@ -619,6 +646,8 @@ export function getMessageAreaStyles(): string {
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getCodeHighlightStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -640,6 +669,7 @@ export function getMessageAreaScript(): string {
|
||||
const waveformIconSvg = \`${waveformIconSvg}\`;
|
||||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||||
|
||||
${getAgentCardScript()}
|
||||
|
||||
@ -669,7 +699,8 @@ export function getMessageAreaScript(): string {
|
||||
'showPlan': searchCodeIconSvg,
|
||||
'addRule': fileWriteIconSvg,
|
||||
'updateNode': fileWriteIconSvg,
|
||||
'addStateTransition': stateTransitionIconSvg
|
||||
'addStateTransition': stateTransitionIconSvg,
|
||||
'askUser': userQuestionIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || '';
|
||||
}
|
||||
@ -689,8 +720,8 @@ export function getMessageAreaScript(): string {
|
||||
'queryKnowledgeSummary': '已查询知识摘要',
|
||||
'queryRules': '已查询规则',
|
||||
'setModule': '已设置模块',
|
||||
'addSignal': '已添加信号',
|
||||
'addSignalExample': '已添加信号示例',
|
||||
'addSignal': '信号分析完成',
|
||||
'addSignalExample': '信号示例处理完成',
|
||||
'validateKnowledgeGraph': '已验证知识图谱',
|
||||
'querySignals': '已查询信号',
|
||||
'addPlan': '已添加计划',
|
||||
@ -699,21 +730,46 @@ export function getMessageAreaScript(): string {
|
||||
'addRule': '已添加规则',
|
||||
'updateNode': '已更新节点',
|
||||
'addStateTransition': '已添加状态转换',
|
||||
'spawnExplorer': '代码探索'
|
||||
'spawnExplorer': '代码探索',
|
||||
'spawnDebugger': '波形调试',
|
||||
'askUser': '用户提问',
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
// 自动滚动控制标志
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
// 检查用户是否在底部附近(允许50px的误差)
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||
}
|
||||
|
||||
// 智能滚动:只有用户在底部附近时才自动滚动
|
||||
// 监听用户滚动行为
|
||||
messagesEl.addEventListener('scroll', () => {
|
||||
const isAtBottom = isUserNearBottom();
|
||||
|
||||
// 如果用户滚动到底部,恢复自动滚动
|
||||
if (isAtBottom) {
|
||||
shouldAutoScroll = true;
|
||||
} else {
|
||||
// 只有当内容高度没有变化时,才认为是用户主动滚动
|
||||
// 如果内容高度变化了,说明是因为新内容导致的位置变化,不应该停止自动滚动
|
||||
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
});
|
||||
|
||||
// 智能滚动:只有在允许自动滚动时才滚动到底部
|
||||
function smartScrollToBottom() {
|
||||
if (isUserNearBottom()) {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@ -896,6 +952,7 @@ export function getMessageAreaScript(): string {
|
||||
// 实时更新分段消息(按后端返回顺序)
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||||
|
||||
if (!segments || segments.length === 0) {
|
||||
console.log('[WebView] segments 为空,跳过渲染');
|
||||
return;
|
||||
@ -936,8 +993,30 @@ export function getMessageAreaScript(): string {
|
||||
// 清空容器并重新渲染所有段落
|
||||
currentSegmentedMessage.innerHTML = '';
|
||||
|
||||
// 合并连续相同的工具调用
|
||||
const mergedSegments = [];
|
||||
let i = 0;
|
||||
while (i < segments.length) {
|
||||
const segment = segments[i];
|
||||
if (segment.type === 'tool') {
|
||||
// 统计连续相同的工具调用
|
||||
let count = 1;
|
||||
while (i + count < segments.length &&
|
||||
segments[i + count].type === 'tool' &&
|
||||
segments[i + count].toolName === segment.toolName) {
|
||||
count++;
|
||||
}
|
||||
// 添加合并后的段落(带计数)
|
||||
mergedSegments.push({ ...segment, toolCount: count });
|
||||
i += count;
|
||||
} else {
|
||||
mergedSegments.push(segment);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
let toolIndex = 0; // 用于跟踪工具段落的索引
|
||||
segments.forEach((segment, index) => {
|
||||
mergedSegments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
|
||||
@ -949,8 +1028,14 @@ export function getMessageAreaScript(): string {
|
||||
if (segment.toolName === 'spawnExplorer') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有工具调用都使用低调样式
|
||||
segmentDiv.className += ' low-profile';
|
||||
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
|
||||
// 检查工具结果是否过长(超过一行显示不下)
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
@ -964,7 +1049,7 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
@ -1038,7 +1123,7 @@ export function getMessageAreaScript(): string {
|
||||
: '';
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-text">\${segment.question || ''}</div>
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
|
||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
|
||||
@ -1162,7 +1247,29 @@ export function getMessageAreaScript(): string {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'message bot-message segmented-message';
|
||||
|
||||
segments.forEach((segment, index) => {
|
||||
// 合并连续相同的工具调用
|
||||
const mergedSegments = [];
|
||||
let i = 0;
|
||||
while (i < segments.length) {
|
||||
const segment = segments[i];
|
||||
if (segment.type === 'tool') {
|
||||
// 统计连续相同的工具调用
|
||||
let count = 1;
|
||||
while (i + count < segments.length &&
|
||||
segments[i + count].type === 'tool' &&
|
||||
segments[i + count].toolName === segment.toolName) {
|
||||
count++;
|
||||
}
|
||||
// 添加合并后的段落(带计数)
|
||||
mergedSegments.push({ ...segment, toolCount: count });
|
||||
i += count;
|
||||
} else {
|
||||
mergedSegments.push(segment);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
mergedSegments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
|
||||
@ -1174,8 +1281,14 @@ export function getMessageAreaScript(): string {
|
||||
if (segment.toolName === 'spawnExplorer') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有工具调用都使用低调样式
|
||||
segmentDiv.className += ' low-profile';
|
||||
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
|
||||
// 检查工具结果是否过长(超过一行显示不下)
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
@ -1183,7 +1296,7 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
||||
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
@ -1238,7 +1351,7 @@ export function getMessageAreaScript(): string {
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-segment">
|
||||
<div class="question-text">\${segment.question || ''}</div>
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
<div class="question-options">
|
||||
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
|
||||
</div>
|
||||
@ -1296,20 +1409,40 @@ export function getMessageAreaScript(): string {
|
||||
function formatText(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// 先转义 HTML 特殊字符
|
||||
let html = text
|
||||
let html = text;
|
||||
|
||||
// 先提取并处理代码块(避免被转义)
|
||||
const codeBlocks = [];
|
||||
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
|
||||
const language = lang || 'plaintext';
|
||||
// 转义代码内容
|
||||
const escapedCode = code.trim()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 处理代码块(三个反引号包裹的代码)
|
||||
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
|
||||
const language = lang || 'plaintext';
|
||||
return '<pre><code class="language-' + language + '">' + code.trim() + '</code></pre>';
|
||||
// 不再手动高亮,让 highlight.js 处理
|
||||
const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
|
||||
codeBlocks.push('<pre><code class="language-' + language + '">' + escapedCode + '</code></pre>');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 处理行内代码(单个反引号包裹)
|
||||
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
||||
// 提取行内代码(避免被转义)
|
||||
const inlineCodes = [];
|
||||
html = html.replace(/\`([^\`]+)\`/g, function(match, code) {
|
||||
const escapedCode = code
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`;
|
||||
inlineCodes.push('<code>' + escapedCode + '</code>');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 转义其他 HTML 特殊字符
|
||||
html = html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 处理标题 ### Title
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||
@ -1332,9 +1465,19 @@ export function getMessageAreaScript(): string {
|
||||
// 处理链接 [text](url)
|
||||
html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
// 处理换行
|
||||
// 处理换行(在恢复代码块之前)
|
||||
html = html.replace(/\\n/g, '<br>');
|
||||
|
||||
// 恢复代码块(在最后恢复,避免被其他处理影响)
|
||||
codeBlocks.forEach((block, index) => {
|
||||
html = html.replace(\`___CODE_BLOCK_\${index}___\`, block);
|
||||
});
|
||||
|
||||
// 恢复行内代码
|
||||
inlineCodes.forEach((code, index) => {
|
||||
html = html.replace(\`___INLINE_CODE_\${index}___\`, code);
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -1484,5 +1627,7 @@ export function getMessageAreaScript(): string {
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
|
||||
${getCodeHighlightScript()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
* 获取模型选择器的 HTML 内容
|
||||
*/
|
||||
export function getModelSelectorContent(
|
||||
autoIcon: string = '',
|
||||
liteIcon: string = '',
|
||||
syIcon: string = '',
|
||||
maxIcon: string = ''
|
||||
autoIcon: string = "",
|
||||
liteIcon: string = "",
|
||||
syIcon: string = "",
|
||||
maxIcon: string = ""
|
||||
): string {
|
||||
return `
|
||||
<!-- 模型选择 -->
|
||||
@ -22,25 +22,51 @@ export function getModelSelectorContent(
|
||||
</svg>
|
||||
</div>
|
||||
<div class="select-dropdown" id="modelDropdown">
|
||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">
|
||||
${autoIcon ? `<img src="${autoIcon}" class="model-icon" alt="Auto">` : ''}
|
||||
<div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
|
||||
${
|
||||
autoIcon
|
||||
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Auto</span>
|
||||
<span class="option-desc">智能匹配最优模型</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">
|
||||
${liteIcon ? `<img src="${liteIcon}" class="model-icon" alt="Lite">` : ''}
|
||||
</div>
|
||||
<div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
|
||||
${
|
||||
liteIcon
|
||||
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Lite</span>
|
||||
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||
${syIcon ? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">` : ''}
|
||||
</div>
|
||||
<div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||
${
|
||||
syIcon
|
||||
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Syntaxic</span>
|
||||
<span class="option-desc">均衡成本和性能,节省credits同时保持可靠输出</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">
|
||||
${maxIcon ? `<img src="${maxIcon}" class="model-icon" alt="Max">` : ''}
|
||||
</div>
|
||||
<div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
|
||||
${
|
||||
maxIcon
|
||||
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
|
||||
: ""
|
||||
}
|
||||
<div class="option-content">
|
||||
<span class="option-label">Max</span>
|
||||
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型选择器的 tooltip 容器 -->
|
||||
<div id="modelTooltip" class="model-tooltip"></div>
|
||||
</div>
|
||||
<span class="tooltiptext">选择模型</span>
|
||||
</div>
|
||||
@ -104,13 +130,13 @@ export function getModelSelectorStyles(): string {
|
||||
/* 模型选择器的选项样式 */
|
||||
#modelDropdown .select-option {
|
||||
position: relative;
|
||||
padding: 6px 12px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
#modelDropdown .select-option:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
@ -125,54 +151,22 @@ export function getModelSelectorStyles(): string {
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
.option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
.option-label {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 模型选择器的 tooltip 样式 */
|
||||
.model-tooltip {
|
||||
position: fixed;
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
.option-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
}
|
||||
.model-tooltip.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
/* tooltip 箭头 */
|
||||
.model-tooltip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 7px;
|
||||
border-style: solid;
|
||||
border-color: transparent rgba(255, 255, 255, 0.2) transparent transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
.model-tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent #1e1e1e transparent transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -235,46 +229,5 @@ export function getModelSelectorScript(): string {
|
||||
function getCurrentModel() {
|
||||
return currentModel;
|
||||
}
|
||||
|
||||
// 模型选择器 tooltip 功能
|
||||
(function initModelTooltip() {
|
||||
const modelDropdown = document.getElementById('modelDropdown');
|
||||
const modelTooltip = document.getElementById('modelTooltip');
|
||||
|
||||
if (!modelDropdown || !modelTooltip) return;
|
||||
|
||||
// 为每个选项添加鼠标事件
|
||||
const options = modelDropdown.querySelectorAll('.select-option');
|
||||
|
||||
options.forEach(option => {
|
||||
option.addEventListener('mouseenter', function(e) {
|
||||
const tooltipText = this.getAttribute('data-tooltip');
|
||||
if (!tooltipText) return;
|
||||
|
||||
// 设置 tooltip 内容
|
||||
modelTooltip.textContent = tooltipText;
|
||||
|
||||
// 获取选项的位置
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
// 计算 tooltip 位置(在选项右侧)
|
||||
const tooltipRect = modelTooltip.getBoundingClientRect();
|
||||
const left = rect.right + 12;
|
||||
const top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
|
||||
|
||||
// 设置位置
|
||||
modelTooltip.style.left = left + 'px';
|
||||
modelTooltip.style.top = top + 'px';
|
||||
|
||||
// 显示 tooltip
|
||||
modelTooltip.classList.add('show');
|
||||
});
|
||||
|
||||
option.addEventListener('mouseleave', function() {
|
||||
// 隐藏 tooltip
|
||||
modelTooltip.classList.remove('show');
|
||||
});
|
||||
});
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
@ -61,10 +61,30 @@ export function getPlanCardStyles(): string {
|
||||
.plan-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.step-num {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 500;
|
||||
margin-right: 6px;
|
||||
.step-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
border: 2px solid var(--vscode-textLink-foreground);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.step-checkbox.completed {
|
||||
background: var(--vscode-textLink-foreground);
|
||||
border-color: var(--vscode-textLink-foreground);
|
||||
opacity: 1;
|
||||
}
|
||||
.step-checkbox.completed::after {
|
||||
content: '✓';
|
||||
color: var(--vscode-editor-background);
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
@ -151,7 +171,7 @@ export function getPlanCardScript(): string {
|
||||
}
|
||||
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
// 选项按钮
|
||||
@ -231,7 +251,7 @@ export function getPlanCardScript(): string {
|
||||
function renderPlanCardStatic(segment, segmentDiv) {
|
||||
segmentDiv.className += ' segment-plan';
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
|
||||
411
src/views/progressBar.ts
Normal file
411
src/views/progressBar.ts
Normal file
@ -0,0 +1,411 @@
|
||||
/**
|
||||
* 进度条模块
|
||||
*
|
||||
* 功能说明:
|
||||
* - 显示开发流程进度: Spec -> Design代码编写 -> 仿真检查 -> AST -> Done
|
||||
* - 支持动态更新当前进度状态
|
||||
* - 提供视觉反馈显示已完成和进行中的步骤
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取进度条的 HTML 内容
|
||||
*/
|
||||
export function getProgressBarContent(): string {
|
||||
return `
|
||||
<div class="progress-bar-container" style="display: none;">
|
||||
<div class="progress-bar-header">
|
||||
<span class="progress-bar-title">开发流程</span>
|
||||
<button class="progress-bar-toggle" title="收起/展开">
|
||||
<span class="toggle-icon">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step" data-step="spec">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Spec</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
|
||||
<div class="progress-step" data-step="design">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Design</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
|
||||
<div class="progress-step" data-step="simulation">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Simulation</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
|
||||
<div class="progress-step" data-step="done">
|
||||
<div class="step-circle">
|
||||
<span class="step-number">4</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Done</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进度条的样式
|
||||
*/
|
||||
export function getProgressBarStyles(): string {
|
||||
return `
|
||||
.progress-bar-container {
|
||||
background: var(--vscode-editor-background);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.progress-bar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.progress-bar-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.progress-bar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.progress-bar-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-bar-container.collapsed .toggle-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 10px 20px;
|
||||
max-height: 60px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-bar-container.collapsed .progress-steps {
|
||||
max-height: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-input-background);
|
||||
border: 2px solid var(--vscode-input-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.step-check {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.step-label {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--vscode-input-border);
|
||||
margin: 0 6px;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
/* 已完成状态 */
|
||||
.progress-step.completed .step-circle {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.progress-step.completed .step-number {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-check {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-label {
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-step.completed + .progress-line {
|
||||
background: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
/* 进行中状态 */
|
||||
.progress-step.active .step-circle {
|
||||
background: var(--vscode-button-background);
|
||||
border-color: var(--vscode-button-background);
|
||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.progress-step.active .step-number {
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.progress-step.active .step-label {
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.progress-steps {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进度条的脚本
|
||||
*/
|
||||
export function getProgressBarScript(): string {
|
||||
return `
|
||||
// 进度条管理
|
||||
const ProgressBar = {
|
||||
steps: ['spec', 'design', 'simulation', 'done'],
|
||||
currentStep: 'spec',
|
||||
isCollapsed: false,
|
||||
|
||||
/**
|
||||
* 初始化进度条
|
||||
*/
|
||||
init() {
|
||||
this.updateProgress('spec');
|
||||
this.initToggle();
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化收起/展开功能
|
||||
*/
|
||||
initToggle() {
|
||||
const container = document.querySelector('.progress-bar-container');
|
||||
const header = document.querySelector('.progress-bar-header');
|
||||
const toggle = document.querySelector('.progress-bar-toggle');
|
||||
|
||||
if (!container || !header || !toggle) return;
|
||||
|
||||
// 点击头部或按钮都可以切换
|
||||
const handleToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
if (this.isCollapsed) {
|
||||
container.classList.add('collapsed');
|
||||
} else {
|
||||
container.classList.remove('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
header.addEventListener('click', handleToggle);
|
||||
toggle.addEventListener('click', handleToggle);
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示进度条
|
||||
*/
|
||||
show() {
|
||||
const container = document.querySelector('.progress-bar-container');
|
||||
if (container) {
|
||||
container.style.display = 'block';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 隐藏进度条
|
||||
*/
|
||||
hide() {
|
||||
const container = document.querySelector('.progress-bar-container');
|
||||
if (container) {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新进度到指定步骤
|
||||
* @param {string} stepName - 步骤名称
|
||||
*/
|
||||
updateProgress(stepName) {
|
||||
if (!this.steps.includes(stepName)) {
|
||||
console.warn('Invalid step name:', stepName);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentStep = stepName;
|
||||
const currentIndex = this.steps.indexOf(stepName);
|
||||
|
||||
// 更新所有步骤的状态
|
||||
document.querySelectorAll('.progress-step').forEach((step, index) => {
|
||||
step.classList.remove('completed', 'active');
|
||||
|
||||
if (index < currentIndex) {
|
||||
step.classList.add('completed');
|
||||
} else if (index === currentIndex) {
|
||||
step.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 更新连接线
|
||||
document.querySelectorAll('.progress-line').forEach((line, index) => {
|
||||
if (index < currentIndex) {
|
||||
line.style.background = 'var(--vscode-button-background)';
|
||||
} else {
|
||||
line.style.background = 'var(--vscode-input-border)';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 前进到下一步
|
||||
*/
|
||||
nextStep() {
|
||||
const currentIndex = this.steps.indexOf(this.currentStep);
|
||||
if (currentIndex < this.steps.length - 1) {
|
||||
this.updateProgress(this.steps[currentIndex + 1]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置进度条
|
||||
*/
|
||||
reset() {
|
||||
this.updateProgress('spec');
|
||||
},
|
||||
|
||||
/**
|
||||
* 完成所有步骤
|
||||
*/
|
||||
complete() {
|
||||
this.updateProgress('done');
|
||||
// 将最后一步也标记为完成
|
||||
const lastStep = document.querySelector('.progress-step[data-step="done"]');
|
||||
if (lastStep) {
|
||||
lastStep.classList.remove('active');
|
||||
lastStep.classList.add('completed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化进度条
|
||||
ProgressBar.init();
|
||||
|
||||
// 监听来自扩展的消息以更新进度
|
||||
window.addEventListener('message', (event) => {
|
||||
const message = event.data;
|
||||
if (message.type === 'updateProgress') {
|
||||
ProgressBar.updateProgress(message.step);
|
||||
} else if (message.type === 'resetProgress') {
|
||||
ProgressBar.reset();
|
||||
} else if (message.type === 'completeProgress') {
|
||||
ProgressBar.complete();
|
||||
} else if (message.type === 'showProgress') {
|
||||
ProgressBar.show();
|
||||
} else if (message.type === 'hideProgress') {
|
||||
ProgressBar.hide();
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
178
src/views/thinkingProcess.ts
Normal file
178
src/views/thinkingProcess.ts
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 思考过程组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 显示 AI 的思考过程
|
||||
* - 支持展开/折叠功能
|
||||
* - 提供打字机效果的流式显示
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取思考过程组件的 HTML 内容
|
||||
* @param thinking - 思考内容
|
||||
* @param isExpanded - 是否默认展开
|
||||
*/
|
||||
export function getThinkingProcessContent(
|
||||
thinking: string = "",
|
||||
isExpanded: boolean = false
|
||||
): string {
|
||||
return `
|
||||
<div class="thinking-process-container ${isExpanded ? "expanded" : ""}">
|
||||
<div class="thinking-header">
|
||||
<div class="thinking-icon-wrapper">
|
||||
<svg class="thinking-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="thinking-title">思考过程</span>
|
||||
</div>
|
||||
<div class="thinking-content">
|
||||
<div class="thinking-text">${thinking || "正在思考中..."}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取思考过程组件的样式
|
||||
*/
|
||||
export function getThinkingProcessStyles(): string {
|
||||
return `
|
||||
.thinking-process-container {
|
||||
margin: 12px 0;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-editor-background);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.thinking-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--vscode-input-background);
|
||||
transition: background 0.2s ease;
|
||||
width: 85px;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thinking-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.1),
|
||||
transparent
|
||||
);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.thinking-process-container.typing .thinking-header::before {
|
||||
animation: shine 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
50%, 100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.thinking-header:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.thinking-icon-wrapper {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.thinking-process-container.expanded .thinking-icon-wrapper {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.thinking-icon {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.thinking-title {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.thinking-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.thinking-process-container.expanded .thinking-content {
|
||||
max-height: 500px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.thinking-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
padding-left: 12px;
|
||||
border-left: 2px solid var(--vscode-textBlockQuote-border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 打字机效果 */
|
||||
.thinking-text.typing::after {
|
||||
content: '▋';
|
||||
animation: blink 1s step-end infinite;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.thinking-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.thinking-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.thinking-content::-webkit-scrollbar-thumb {
|
||||
background: var(--vscode-scrollbarSlider-background);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.thinking-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
287
src/views/userInfoComponent.ts
Normal file
287
src/views/userInfoComponent.ts
Normal file
@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 用户信息组件
|
||||
* 包含用户头像、昵称、会员等级等信息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 HTML 内容
|
||||
* 只包含用户详情下拉面板,不包含触发按钮
|
||||
*/
|
||||
export function getUserInfoComponentContent(): string {
|
||||
return `
|
||||
<div class="user-info-wrapper">
|
||||
<!-- 用户详情下拉面板 -->
|
||||
<div class="user-detail-dropdown" id="userDetailDropdown">
|
||||
<div class="user-detail-content">
|
||||
<div class="user-detail-header">
|
||||
<div class="user-avatar-small">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-name-tier">
|
||||
<div class="user-detail-name" id="userDetailName">加载中...</div>
|
||||
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-detail-body">
|
||||
<div class="user-detail-item">
|
||||
<span class="detail-label">剩余 Credits</span>
|
||||
<span class="detail-value" id="creditsDetail">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 CSS 样式
|
||||
*/
|
||||
export function getUserInfoComponentStyles(): string {
|
||||
return `
|
||||
.user-info-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 用户详情下拉面板 */
|
||||
.user-detail-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
min-width: 250px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.user-detail-dropdown.active {
|
||||
display: block;
|
||||
animation: dropdownSlideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.user-detail-content {
|
||||
background: var(--vscode-sideBar-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-detail-header {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: linear-gradient(135deg, rgba(0, 122, 204, 0.1) 0%, rgba(88, 166, 255, 0.05) 100%);
|
||||
border-bottom: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #007acc 0%, #58a6ff 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.user-avatar-small svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.user-name-tier {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-detail-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.tier-icon-inline {
|
||||
height: 26px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.user-detail-body {
|
||||
padding: 12px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
}
|
||||
|
||||
.user-detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--vscode-editor-background);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-detail-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-color: rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.user-detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tier-icon-large {
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.tier-icon {
|
||||
width: 110px;
|
||||
height: 35px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息组件的 JavaScript 脚本
|
||||
*/
|
||||
export function getUserInfoComponentScript(): string {
|
||||
return `
|
||||
// 用户信息数据
|
||||
let currentUserInfo = null;
|
||||
|
||||
// 切换用户详情下拉面板
|
||||
function openUserDetailModal() {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
const userButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (dropdown) {
|
||||
const isActive = dropdown.classList.contains('active');
|
||||
if (isActive) {
|
||||
dropdown.classList.remove('active');
|
||||
if (userButton) {
|
||||
userButton.classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
dropdown.classList.add('active');
|
||||
if (userButton) {
|
||||
userButton.classList.add('active');
|
||||
}
|
||||
// 更新下拉面板中的用户信息
|
||||
updateUserDetailModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭用户详情下拉面板
|
||||
function closeUserDetailModal() {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
const userButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('active');
|
||||
}
|
||||
if (userButton) {
|
||||
userButton.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户详情下拉面板内容
|
||||
function updateUserDetailModal() {
|
||||
if (!currentUserInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新用户名
|
||||
const userDetailName = document.getElementById('userDetailName');
|
||||
if (userDetailName) {
|
||||
userDetailName.textContent = currentUserInfo.nickname || '未知用户';
|
||||
}
|
||||
|
||||
// 更新会员等级图标(显示在用户名旁边)
|
||||
const tierIconInline = document.getElementById('tierIconInline');
|
||||
if (tierIconInline && currentUserInfo.tierIconUrl) {
|
||||
tierIconInline.src = currentUserInfo.tierIconUrl;
|
||||
tierIconInline.style.display = 'block';
|
||||
} else if (tierIconInline) {
|
||||
tierIconInline.style.display = 'none';
|
||||
}
|
||||
|
||||
// 更新剩余 Credits
|
||||
const creditsDetail = document.getElementById('creditsDetail');
|
||||
if (creditsDetail) {
|
||||
creditsDetail.textContent = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息显示
|
||||
function updateUserInfoDisplay(userInfo) {
|
||||
currentUserInfo = userInfo;
|
||||
}
|
||||
|
||||
// 绑定下拉面板事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 点击页面其他地方关闭下拉面板
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('userDetailDropdown');
|
||||
const userButton = document.getElementById('userAvatarIconButton');
|
||||
|
||||
if (dropdown && dropdown.classList.contains('active')) {
|
||||
// 如果点击的不是用户按钮和下拉面板内容,则关闭
|
||||
if (!userButton?.contains(e.target) && !dropdown.contains(e.target)) {
|
||||
closeUserDetailModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止下拉面板内容点击事件冒泡
|
||||
const dropdownContent = document.querySelector('.user-detail-content');
|
||||
if (dropdownContent) {
|
||||
dropdownContent.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
@ -5,65 +5,79 @@ export function getWaveformPreviewContent(): string {
|
||||
return `
|
||||
/* 波形预览组件样式 */
|
||||
.waveform-preview {
|
||||
margin-top: 12px;
|
||||
margin: 16px 0;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
.waveform-preview:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.waveform-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, var(--vscode-input-background) 0%, var(--vscode-editor-background) 100%);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.waveform-preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.waveform-preview-title svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--vscode-button-background);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
.waveform-expand-btn {
|
||||
padding: 4px 12px;
|
||||
padding: 6px 14px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: opacity 0.2s ease;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.waveform-expand-btn:hover {
|
||||
opacity: 0.9;
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.waveform-expand-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.waveform-expand-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.waveform-preview-content {
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
max-height: 300px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.waveform-preview-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
height: auto;
|
||||
}
|
||||
.waveform-preview-placeholder {
|
||||
display: flex;
|
||||
@ -88,7 +102,8 @@ export function getWaveformPreviewContent(): string {
|
||||
}
|
||||
.waveform-mini-viewer {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: auto;
|
||||
min-height: 120px;
|
||||
background: var(--vscode-editor-background);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@ -332,7 +347,7 @@ export function getWaveformPreviewScript(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开完整波形查看器
|
||||
* 打开完整波形查看器(在新列中)
|
||||
*/
|
||||
function openFullWaveform(vcdFilePath) {
|
||||
vscode.postMessage({
|
||||
|
||||
@ -18,6 +18,13 @@ import {
|
||||
getMessageAreaScript,
|
||||
} from "./messageArea";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
import {
|
||||
getProgressBarContent,
|
||||
getProgressBarStyles,
|
||||
getProgressBarScript,
|
||||
} from "./progressBar";
|
||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||
import { getCurrentEnv } from "../config/settings";
|
||||
/**
|
||||
* 获取 WebView 面板的 HTML 内容
|
||||
*/
|
||||
@ -28,12 +35,17 @@ export function getWebviewContent(
|
||||
syIconUri?: string,
|
||||
maxIconUri?: string
|
||||
): string {
|
||||
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
|
||||
const currentEnv = getCurrentEnv();
|
||||
const showQuickActions = currentEnv === "dev" || currentEnv === "test";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IC Coder</title>
|
||||
${getHighlightJsLinks()}
|
||||
<style>
|
||||
body {
|
||||
font-family: var(--vscode-font-family);
|
||||
@ -75,6 +87,7 @@ export function getWebviewContent(
|
||||
${getAgentCardStyles()}
|
||||
${getWaveformPreviewContent()}
|
||||
${getConversationHistoryBarStyles()}
|
||||
${getProgressBarStyles()}
|
||||
${getInputAreaStyles()}
|
||||
|
||||
.file-editor-section {
|
||||
@ -258,7 +271,7 @@ export function getWebviewContent(
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -301,7 +314,7 @@ export function getWebviewContent(
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 14px;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.question-segment .question-text {
|
||||
@ -380,6 +393,7 @@ export function getWebviewContent(
|
||||
</head>
|
||||
<body>
|
||||
${getConversationHistoryBarContent()}
|
||||
${getProgressBarContent()}
|
||||
<div class="header">
|
||||
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
|
||||
@ -397,12 +411,16 @@ export function getWebviewContent(
|
||||
<span id="statusText">思考中...</span>
|
||||
</div>
|
||||
|
||||
<!-- <div class="quick-actions">
|
||||
${
|
||||
showQuickActions
|
||||
? `<div class="quick-actions">
|
||||
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
||||
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
||||
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
||||
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
|
||||
</div> -->
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
|
||||
</div>
|
||||
@ -443,10 +461,9 @@ export function getWebviewContent(
|
||||
}
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': '只读模式 - 只能查询分析',
|
||||
'ask': '逐个确认 - 每个写操作需确认',
|
||||
'agent': '智能体自主模式',
|
||||
'auto': '完全自动 - 所有操作自动执行'
|
||||
'plan': 'plan模式',
|
||||
'ask': 'ask模式',
|
||||
'agent': 'agent模式'
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
@ -568,6 +585,27 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'updateUserInfo':
|
||||
// 更新用户信息
|
||||
console.log('[WebView] 收到用户信息:', message.userInfo);
|
||||
if (message.userInfo) {
|
||||
const userInfoData = {
|
||||
nickname: message.userInfo.nickname || message.userInfo.username || '用户',
|
||||
userId: message.userInfo.userId || message.userInfo.id,
|
||||
tierName: message.userInfo.tierName,
|
||||
tierIconUrl: message.tierIconUrl,
|
||||
registerTime: message.userInfo.registerTime || message.userInfo.createdAt
|
||||
};
|
||||
|
||||
console.log('[WebView] 显示用户信息:', userInfoData);
|
||||
|
||||
// 调用更新用户头像图标按钮的函数
|
||||
if (typeof updateUserAvatarIconButton === 'function') {
|
||||
updateUserAvatarIconButton(userInfoData);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resetSegmentedMessage':
|
||||
// 重置分段消息容器(停止对话时调用)
|
||||
console.log('[WebView] 重置分段消息容器');
|
||||
@ -650,6 +688,10 @@ export function getWebviewContent(
|
||||
if (messagesContainer) {
|
||||
messagesContainer.innerHTML = '';
|
||||
}
|
||||
// 重置输入框布局到居中
|
||||
if (typeof window.resetInputAreaLayout === 'function') {
|
||||
window.resetInputAreaLayout();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addUserMessage':
|
||||
@ -657,6 +699,10 @@ export function getWebviewContent(
|
||||
if (message.text) {
|
||||
addMessage(message.text, 'user');
|
||||
}
|
||||
// 检查并更新输入框布局
|
||||
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
|
||||
window.checkMessagesAndUpdateLayout();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addAiMessage':
|
||||
@ -664,6 +710,10 @@ export function getWebviewContent(
|
||||
if (message.text) {
|
||||
addMessage(message.text, 'bot');
|
||||
}
|
||||
// 检查并更新输入框布局
|
||||
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
|
||||
window.checkMessagesAndUpdateLayout();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'switchMode':
|
||||
@ -696,6 +746,7 @@ export function getWebviewContent(
|
||||
${getAgentCardScript()}
|
||||
${getWaveformPreviewScript()}
|
||||
${getConversationHistoryBarScript()}
|
||||
${getProgressBarScript()}
|
||||
${getInputAreaScript()}
|
||||
</script></body>
|
||||
</html>`;
|
||||
|
||||
Reference in New Issue
Block a user