feat: Premium UI overhaul, brain management, history persistence, and IME fix
This commit is contained in:
+43
-31
@@ -465,25 +465,32 @@ export class AgentExecutor {
|
||||
return [
|
||||
`제가 실제로 \`${projectPath}\` 폴더를 읽고 1차 분석했습니다.`,
|
||||
'',
|
||||
'**제품 설명**',
|
||||
pkg?.name
|
||||
? `- 이 프로젝트는 \`${pkg.name}\`${pkg.description ? ` (${pkg.description})` : ''}로 식별됩니다.`
|
||||
: '- `package.json` 기준의 제품명은 확인되지 않았습니다.',
|
||||
readmeSummary ? `- README 기준 핵심 설명: ${readmeSummary}` : '- README 기반 제품 설명은 부족합니다. 제품 소개 문서를 보강하는 편이 좋습니다.',
|
||||
stack.length ? `- 감지된 기술 스택: ${stack.join(', ')}` : '- 기술 스택은 파일 구조만으로는 명확하지 않습니다.',
|
||||
'### 📋 제품 개요',
|
||||
'| 항목 | 내용 |',
|
||||
'| :--- | :--- |',
|
||||
pkg?.name ? `| **제품명** | \`${pkg.name}\` |` : '| **제품명** | 식별되지 않음 |',
|
||||
pkg?.description ? `| **설명** | ${pkg.description} |` : '| **설명** | - |',
|
||||
stack.length ? `| **기술 스택** | ${stack.join(', ')} |` : '| **기술 스택** | 파악 중 |',
|
||||
readmeSummary ? `| **핵심 요약** | ${readmeSummary} |` : '| **핵심 요약** | README 정보 부족 |',
|
||||
'',
|
||||
'**설계 구조**',
|
||||
topDirs.length ? topDirs.map(item => `- ${item}`).join('\n') : '- 상위 디렉터리 구조가 단순하거나 비어 있습니다.',
|
||||
entryPoints.length ? `- 주요 진입점 후보: ${entryPoints.join(', ')}` : '- 명확한 앱 진입점은 아직 식별되지 않았습니다.',
|
||||
configFiles.length ? `- 주요 설정 파일: ${configFiles.slice(0, 12).join(', ')}` : '- 주요 설정 파일이 많지 않습니다.',
|
||||
'### 🏗️ 설계 구조',
|
||||
'| 디렉토리 | 파일 수 |',
|
||||
'| :--- | :--- |',
|
||||
...(topDirs.length ? topDirs : ['| - | - |']),
|
||||
'',
|
||||
'**코드 리뷰 관점의 1차 소견**',
|
||||
reviewFindings.join('\n'),
|
||||
'**주요 진입점 및 설정**',
|
||||
entryPoints.length ? `- 진입점: ${entryPoints.map(e => `\`${e}\``).join(', ')}` : '- 명확한 진입점 미식별',
|
||||
configFiles.length ? `- 설정: ${configFiles.slice(0, 8).map(f => `\`${f}\``).join(', ')}` : '- 주요 설정 파일 없음',
|
||||
'',
|
||||
'**다음에 더 깊게 볼 부분**',
|
||||
'- 실제 비즈니스 플로우는 `src`, `app`, `lib`, `components` 내부 핵심 파일을 2차로 읽어야 정확히 판단할 수 있습니다.',
|
||||
'- 지금 단계에서는 “아무것도 안 하고 말만 하는” 답변이 아니라, 실제 파일 시스템을 기준으로 프로젝트 형태를 먼저 파악한 결과입니다.',
|
||||
'- 원하시면 다음 턴에서 제가 핵심 소스 파일을 더 읽어 아키텍처 다이어그램 수준으로 이어서 정리하겠습니다.'
|
||||
'### 🔍 코드 리뷰 1차 소견',
|
||||
...reviewFindings.map(f => f.startsWith('-') ? f : `- ${f}`),
|
||||
'',
|
||||
'### 🚀 향후 분석 제안',
|
||||
'1. **비즈니스 로직 분석**: `src`, `app`, `lib` 내부의 핵심 비즈니스 흐름 파악',
|
||||
'2. **아키텍처 시각화**: 파일 간 의존성 및 데이터 흐름 다이어그램 작성',
|
||||
'3. **상세 코드 리뷰**: 특정 파일이나 기능에 대한 심층 분석',
|
||||
'',
|
||||
'*분석을 계속하시려면 궁금한 파일이나 모듈을 말씀해 주세요.*'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -521,7 +528,7 @@ export class AgentExecutor {
|
||||
.slice(0, 12)
|
||||
.map(entry => {
|
||||
const count = this.collectProjectFiles(path.join(projectPath, entry.name), 200, projectPath).length;
|
||||
return `\`${entry.name}/\`: 약 ${count}개 파일`;
|
||||
return `| \`${entry.name}/\` | 약 ${count}개 |`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,27 +645,32 @@ export class AgentExecutor {
|
||||
.map((entry) => {
|
||||
const dirPath = path.join(brainDir, entry.name);
|
||||
const count = findBrainFiles(dirPath).length;
|
||||
return `- ${entry.name}/: ${count}개 문서`;
|
||||
return `| \`${entry.name}/\` | ${count}개 문서 |`;
|
||||
});
|
||||
|
||||
const fileSummaries = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.slice(0, 8)
|
||||
.slice(0, 10)
|
||||
.map((entry) => `- ${entry.name}`);
|
||||
|
||||
const sampleFiles = files
|
||||
.slice(0, 10)
|
||||
.map((file) => `- ${path.relative(brainDir, file)}`);
|
||||
|
||||
const sections = [
|
||||
`현재 연결된 제2뇌는 **${activeBrain.name}**입니다.`,
|
||||
`경로: \`${brainDir}\``,
|
||||
`총 Markdown 지식 문서: **${files.length}개**`,
|
||||
activeBrain.description ? `설명: ${activeBrain.description}` : '',
|
||||
directorySummaries.length ? `\n상위 폴더 구조는 이렇게 보입니다.\n${directorySummaries.join('\n')}` : '',
|
||||
fileSummaries.length ? `\n루트에 있는 주요 문서는 이런 것들이 있습니다.\n${fileSummaries.join('\n')}` : '',
|
||||
sampleFiles.length ? `\n제가 우선 참고할 수 있는 샘플 문서는 다음과 같습니다.\n${sampleFiles.join('\n')}` : '',
|
||||
`\n이 자료는 충분히 도움이 됩니다. 앞으로 질문을 받으면 먼저 이 제2뇌에서 관련 기준이나 맥락을 찾고, 부족할 때만 제 일반 지식으로 보완하겠습니다.`
|
||||
`현재 연결된 제2뇌 **${activeBrain.name}**의 개요입니다.`,
|
||||
'',
|
||||
'### 🧠 지식 베이스 현황',
|
||||
'| 항목 | 정보 |',
|
||||
'| :--- | :--- |',
|
||||
`| **위치** | \`${brainDir}\` |`,
|
||||
`| **문서 총계** | **${files.length}개** |`,
|
||||
activeBrain.description ? `| **설명** | ${activeBrain.description} |` : '| **설명** | 설정된 설명 없음 |',
|
||||
'',
|
||||
'### 📂 주요 카테고리',
|
||||
'| 폴더명 | 문서 수 |',
|
||||
'| :--- | :--- |',
|
||||
...(directorySummaries.length ? directorySummaries : ['| - | - |']),
|
||||
'',
|
||||
fileSummaries.length ? `### 📄 최근/주요 문서\n${fileSummaries.join('\n')}` : '',
|
||||
'',
|
||||
'이 자료들을 기반으로 답변을 최적화하겠습니다. 특정 주제에 대해 깊이 있는 분석이 필요하시면 말씀해 주세요.'
|
||||
].filter(Boolean);
|
||||
|
||||
return sections.join('\n');
|
||||
|
||||
+492
-346
@@ -118,6 +118,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
await this.syncBrain();
|
||||
await this._sendBrainStatus();
|
||||
break;
|
||||
case 'addBrain':
|
||||
await this._addBrainProfile();
|
||||
break;
|
||||
case 'setBrainProfile':
|
||||
await this._setActiveBrainProfile(data.id);
|
||||
break;
|
||||
@@ -644,367 +647,510 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
|
||||
private _getHtml(webview: vscode.Webview): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>G1nation</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{
|
||||
--bg:#000000;--bg2:#050505;--surface:rgba(0,18,5,.75);--surface2:rgba(0,35,10,.6);
|
||||
--border:rgba(255,255,255,.08);--border2:rgba(255,255,255,.12);
|
||||
--text:#A1A1AA;--text-bright:#FFFFFF;--text-dim:#71717A;
|
||||
--accent:#00FF41;--accent2:#008F11;--accent3:#00FF41;
|
||||
--accent-glow:rgba(0,255,65,.25);--accent2-glow:rgba(0,143,17,.2);
|
||||
--input-bg:rgba(0,10,2,.9);--code-bg:#020502;
|
||||
--green:#00FF41;--yellow:#ffab40;--cyan:#00e5ff;--red:#ff5252;
|
||||
}
|
||||
body.vscode-light {
|
||||
--bg:#fafafa;--bg2:#ffffff;--surface:rgba(255,255,255,.8);--surface2:rgba(240,240,245,.8);
|
||||
--border:rgba(0,0,0,.08);--border2:rgba(0,0,0,.15);
|
||||
--text:#454555;--text-bright:#111118;--text-dim:#888899;
|
||||
--accent-glow:rgba(124,106,255,.1);--accent2-glow:rgba(224,64,251,.08);
|
||||
--input-bg:rgba(255,255,255,.9);--code-bg:#f5f5f7;
|
||||
}
|
||||
html,body{height:100%;font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;font-size:13px;background:var(--bg);color:var(--text);display:flex;flex-direction:column;overflow:hidden;min-height:0}
|
||||
body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(ellipse at 20% 50%,rgba(124,106,255,.06) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(224,64,251,.04) 0%,transparent 50%),radial-gradient(ellipse at 50% 80%,rgba(0,229,255,.03) 0%,transparent 50%);animation:aurora 20s ease-in-out infinite;z-index:0;pointer-events:none}
|
||||
@keyframes aurora{0%,100%{transform:translate(0,0) rotate(0deg)}33%{transform:translate(2%,-1%) rotate(.5deg)}66%{transform:translate(-1%,2%) rotate(-.5deg)}}
|
||||
.header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:rgba(10,10,12,.8);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);flex-shrink:0;position:relative;z-index:10}
|
||||
.header::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 5%,var(--accent) 30%,var(--accent2) 50%,var(--accent3) 70%,transparent 95%);opacity:.5;animation:headerGlow 4s ease-in-out infinite alternate}
|
||||
@keyframes headerGlow{0%{opacity:.3}100%{opacity:.6}}
|
||||
.thinking-bar{height:2px;background:transparent;position:relative;overflow:hidden;flex-shrink:0;z-index:10}
|
||||
.thinking-bar.active{background:rgba(124,106,255,.1)}
|
||||
.thinking-bar.active::after{content:'';position:absolute;top:0;left:-40%;width:40%;height:100%;background:linear-gradient(90deg,transparent,var(--accent),var(--accent2),var(--accent3),transparent);animation:thinkSlide 1.5s ease-in-out infinite}
|
||||
@keyframes thinkSlide{0%{left:-40%}100%{left:100%}}
|
||||
.brain-strip{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 14px;border-bottom:1px solid var(--border);background:rgba(0,10,2,.64);z-index:8;position:relative}
|
||||
.brain-strip-main{font-size:11px;color:var(--accent);font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.brain-strip-sub{font-size:10px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}
|
||||
.header-left{display:flex;align-items:center;gap:8px}
|
||||
.logo{width:26px;height:26px;border-radius:6px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);box-shadow:0 0 15px rgba(0,255,65,.15);animation:logoPulse 3s ease-in-out infinite;position:relative;text-shadow:0 0 8px var(--accent)}
|
||||
@keyframes logoPulse{0%,100%{box-shadow:0 0 10px rgba(0,255,65,.1)}50%{box-shadow:0 0 25px rgba(0,255,65,.3)}}
|
||||
.brand{font-weight:800;font-size:14px;color:var(--text-bright);letter-spacing:-.5px;background:linear-gradient(135deg,#fff 40%,var(--accent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.header-right{display:flex;align-items:center;gap:5px;flex-wrap:wrap;justify-content:flex-end}
|
||||
.select-wrap{display:flex;align-items:center;gap:5px}
|
||||
.select-wrap select{max-width:140px}
|
||||
.brain-meta{font-size:10px;color:var(--text-dim);max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.history-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);backdrop-filter:blur(15px);z-index:100;display:none;flex-direction:column;padding:20px;animation:fadeIn 0.3s ease-out}
|
||||
.history-overlay.visible{display:flex}
|
||||
.history-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
|
||||
.history-title{font-size:18px;font-weight:bold;color:var(--accent)}
|
||||
.history-list{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:10px}
|
||||
.history-item{background:rgba(255,255,255,0.05);border:1px solid var(--border);padding:12px;border-radius:10px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:all 0.2s}
|
||||
.history-item:hover{border-color:var(--accent);background:rgba(0,255,65,0.05)}
|
||||
.history-item-info{display:flex;flex-direction:column;gap:4px;flex:1}
|
||||
.history-item-title{font-weight:600;font-size:12px;color:var(--text-bright)}
|
||||
.history-item-date{font-size:10px;color:var(--text-dim)}
|
||||
.history-del-btn{background:transparent;border:none;color:var(--text-dim);cursor:pointer;padding:8px;font-size:14px;transition:color 0.2s}
|
||||
.history-del-btn:hover{color:var(--red)}
|
||||
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||||
select{background:rgba(22,22,28,.9);color:var(--text-bright);border:1px solid var(--border2);padding:5px 8px;border-radius:8px;font-size:10px;cursor:pointer;outline:none;max-width:120px;transition:all .3s;backdrop-filter:blur(8px)}
|
||||
select:hover,select:focus{border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)}
|
||||
.btn-icon{background:rgba(22,22,28,.7);border:1px solid var(--border2);color:var(--text-dim);width:28px;height:28px;border-radius:8px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s;backdrop-filter:blur(8px);position:relative;overflow:hidden}
|
||||
.btn-icon:hover{color:var(--text-bright);border-color:var(--accent);transform:translateY(-1px);box-shadow:0 4px 15px var(--accent-glow)}
|
||||
.chat{flex:1;overflow-y:auto;padding:16px 14px;display:flex;flex-direction:column;gap:16px;position:relative;z-index:1;min-height:0}
|
||||
.chat::-webkit-scrollbar{width:2px}.chat::-webkit-scrollbar-track{background:transparent}.chat::-webkit-scrollbar-thumb{background:var(--accent);border-radius:2px;opacity:.5}
|
||||
.msg{display:flex;flex-direction:column;gap:5px;animation:msgIn .5s cubic-bezier(.16,1,.3,1)}
|
||||
.msg-head{display:flex;align-items:center;gap:7px;font-weight:600;font-size:11px;color:var(--text)}
|
||||
.msg-time{font-weight:400;font-size:9px;color:var(--text-dim);margin-left:auto;opacity:.6}
|
||||
.av{width:22px;height:22px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0}
|
||||
.av-user{background:var(--surface2);color:var(--text);border:1px solid var(--border2)}
|
||||
.av-ai{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 0 10px rgba(124,106,255,.3)}
|
||||
.msg-body{padding-left:29px;line-height:1.75;color:var(--text);white-space:pre-wrap;word-break:break-word;font-size:13px}
|
||||
.msg-user .msg-body{background:var(--surface);border:1px solid var(--border2);border-radius:14px;padding:10px 14px;margin-left:29px;color:var(--text-bright);backdrop-filter:blur(8px)}
|
||||
.msg-body pre{background:var(--code-bg);border:1px solid var(--border2);border-radius:10px;padding:14px 16px;overflow-x:auto;margin:8px 0;font-size:12px;line-height:1.6;color:#c9d1d9;position:relative}
|
||||
.msg-body code{font-family:'SF Mono','JetBrains Mono','Fira Code','Menlo',monospace;font-size:11.5px}
|
||||
.msg-body :not(pre)>code{background:rgba(124,106,255,.1);color:var(--accent);padding:2px 7px;border-radius:5px;border:1px solid rgba(124,106,255,.15)}
|
||||
.code-wrap{position:relative}
|
||||
.code-lang{position:absolute;top:0;left:14px;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;padding:2px 10px;border-radius:0 0 6px 6px;font-size:9px;font-family:'SF Mono',monospace;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
|
||||
.copy-btn{position:absolute;top:8px;right:8px;background:var(--surface2);border:1px solid var(--border2);color:var(--text-dim);padding:4px 12px;border-radius:6px;font-size:10px;cursor:pointer;opacity:0;transition:all .3s;z-index:1;backdrop-filter:blur(8px)}
|
||||
.code-wrap:hover .copy-btn{opacity:1}.copy-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)}
|
||||
.welcome{text-align:center;padding:0 20px 20px;position:relative}
|
||||
.welcome-logo{width:56px;height:56px;border-radius:16px;margin:0 auto 16px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:32px;color:var(--accent);box-shadow:inset 0 0 15px rgba(0,255,65,.1), 0 0 30px rgba(0,255,65,.2);animation:welcomeFloat 4s ease-in-out infinite;position:relative;text-shadow:0 0 15px var(--accent)}
|
||||
@keyframes welcomeFloat{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-6px) scale(1.03)}}
|
||||
.welcome-title{font-size:22px;font-weight:900;letter-spacing:-1px;color:var(--text-bright);margin-bottom:8px}
|
||||
.welcome-sub{color:var(--text-dim);font-size:12px;line-height:1.7;margin-bottom:18px;letter-spacing:-.2px}
|
||||
.loading-wrap{padding-left:29px;padding-top:6px;display:flex;align-items:center;gap:10px}
|
||||
.loading-dots{display:flex;gap:4px}
|
||||
.loading-dots span{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:dotBounce 1.4s ease-in-out infinite}
|
||||
.loading-dots span:nth-child(2){animation-delay:.2s;background:var(--accent2)}
|
||||
.loading-dots span:nth-child(3){animation-delay:.4s;background:var(--accent3)}
|
||||
@keyframes dotBounce{0%,80%,100%{transform:scale(.6);opacity:.4}40%{transform:scale(1.2);opacity:1}}
|
||||
.loading-text{font-size:11px;color:var(--text-dim);animation:pulse 2s ease-in-out infinite;letter-spacing:.3px}
|
||||
.auto-status{margin:12px 0 4px 29px;padding:10px 12px;border:1px solid rgba(255,255,255,.16);border-radius:10px;background:rgba(0,255,65,.08);color:#fff;font-size:13px;font-weight:700;line-height:1.5;box-shadow:0 0 18px rgba(0,255,65,.12)}
|
||||
.input-wrap{padding:8px 14px 14px;flex-shrink:0;position:relative;z-index:1}
|
||||
.input-box{background:var(--input-bg);border:1px solid var(--border2);border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:all .3s;position:relative;backdrop-filter:blur(12px)}
|
||||
.input-box:focus-within{border-color:var(--accent);box-shadow:0 0 24px rgba(0,255,65,.15)}
|
||||
textarea{width:100%;background:transparent;border:none;color:var(--text-bright);font-family:inherit;font-size:13px;line-height:1.5;resize:none;outline:none;min-height:22px;max-height:150px}
|
||||
textarea::placeholder{color:var(--text-dim)}
|
||||
.input-footer{display:flex;align-items:center;justify-content:space-between}
|
||||
.input-hint{font-size:10px;color:var(--text-dim);opacity:.5}
|
||||
.input-btns{display:flex;gap:5px}
|
||||
.send-btn{background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff;width:32px;height:32px;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .2s;box-shadow:0 2px 12px rgba(124,106,255,.35);position:relative;overflow:hidden}
|
||||
.send-btn:hover{transform:translateY(-2px) scale(1.05);box-shadow:0 6px 24px rgba(124,106,255,.45)}
|
||||
.send-btn:active{transform:scale(.92)}.send-btn:disabled{opacity:.2;cursor:not-allowed}
|
||||
.stop-btn{background:var(--red);border:none;color:#fff;width:32px;height:32px;border-radius:10px;cursor:pointer;display:none;align-items:center;justify-content:center;font-size:11px;box-shadow:0 0 12px rgba(255,82,82,.3)}
|
||||
.stop-btn.visible{display:flex}
|
||||
@keyframes msgIn{from{opacity:0;transform:translateY(12px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||||
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
|
||||
.stream-active::after{content:'';display:inline-block;width:2px;height:14px;background:var(--accent);margin-left:2px;animation:blink .6s step-end infinite;vertical-align:text-bottom;box-shadow:0 0 6px var(--accent)}
|
||||
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
|
||||
.main-view{flex:1;display:flex;flex-direction:column;overflow:hidden;transition:all .5s cubic-bezier(.16,1,.3,1);min-height:0;max-height:100%}
|
||||
body.init .main-view{justify-content:center;margin-top:-6vh}
|
||||
body.init .chat{flex:0 0 auto;overflow:visible;padding-bottom:15px}
|
||||
body.init .input-wrap{max-width:680px;width:100%;margin:0 auto}
|
||||
.attach-btn{background:transparent;border:1px solid var(--border2);color:var(--text-dim);width:32px;height:32px;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .3s;flex-shrink:0}
|
||||
.attach-btn:hover{color:var(--accent);border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)}
|
||||
.attach-preview{display:none;gap:6px;padding:0 0 6px;flex-wrap:wrap}
|
||||
.attach-preview.visible{display:flex}
|
||||
.attach-chip{display:flex;align-items:center;gap:5px;background:var(--surface2);border:1px solid var(--border2);border-radius:8px;padding:4px 10px;font-size:10px;color:var(--text)}
|
||||
.attach-chip .chip-remove{cursor:pointer;color:var(--text-dim);font-size:12px;margin-left:2px}
|
||||
.attach-chip .chip-remove:hover{color:var(--red)}
|
||||
.attach-thumb{width:28px;height:28px;border-radius:5px;object-fit:cover;border:1px solid var(--border2)}
|
||||
.regen-btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:none;color:var(--text-dim);padding:4px 6px;border-radius:4px;font-size:11px;cursor:pointer;transition:color 0.2s;margin-top:6px;margin-left:29px;opacity:0.7}
|
||||
.regen-btn:hover{color:var(--text);opacity:1}
|
||||
</style></head><body class="init">
|
||||
<div class="header"><div class="header-left"><div class="logo">✦</div><span class="brand">G1nation</span></div><div class="header-right"><div id="engineStatusDot" style="width:8px;height:8px;border-radius:50%;background:var(--text-dim);margin-right:-2px" title="Checking AI Engine..."></div><div class="select-wrap"><select id="modelSel" title="Active AI model"></select><button class="btn-icon" id="refreshModelsBtn" title="Refresh Models">↻</button></div><div class="select-wrap"><select id="brainSel" title="Active Second Brain"></select><button class="btn-icon" id="manageBrainsBtn" title="Add or manage brains">✎</button></div><button class="btn-icon" id="internetBtn" title="Internet Access: OFF">🌐</button><button class="btn-icon" id="brainBtn" title="Sync Second Brain">🧠<span id="brainCountBadge" style="position:absolute;top:-5px;right:-5px;background:var(--accent);color:#000;font-size:8px;padding:1px 3px;border-radius:4px;display:none;font-weight:bold">0</span></button><button class="btn-icon" id="historyBtn" title="Chat History">📜</button><button class="btn-icon" id="settingsBtn" title="Settings">⚙️</button><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
|
||||
<div id="historyOverlay" class="history-overlay">
|
||||
<div class="history-header"><span class="history-title">Chat Sessions</span><button class="btn-icon" id="closeHistoryBtn">✕</button></div>
|
||||
<div class="history-list" id="historyList"></div>
|
||||
</div>
|
||||
<div class="thinking-bar" id="thinkingBar"></div>
|
||||
<div class="brain-strip"><div id="brainStatusInfo" class="brain-strip-main">Brain: checking...</div><div id="brainStatusMeta" class="brain-strip-sub"></div></div>
|
||||
<div class="main-view" id="mainView">
|
||||
<div class="chat" id="chat">
|
||||
<div class="welcome">
|
||||
<div class="welcome-logo">✦</div>
|
||||
<div class="welcome-title">G1nation</div>
|
||||
<div class="welcome-sub">Security · Optimized · Knowledge Mesh<br>Choose a brain above, then ask what it knows.</div>
|
||||
</div></div>
|
||||
<div class="input-wrap"><div class="input-box">
|
||||
<div class="attach-preview" id="attachPreview"></div>
|
||||
<textarea id="input" rows="1" placeholder="What shall we build today?"></textarea>
|
||||
<div class="input-footer"><span class="input-hint">Enter to Send · Shift+Enter for New Line</span>
|
||||
<div class="input-btns"><button class="attach-btn" id="attachBtn" title="Attach Files">+</button><button class="stop-btn" id="stopBtn">■</button><button class="send-btn" id="sendBtn">↑</button></div></div></div>
|
||||
<input type="file" id="fileInput" multiple accept="image/*,.txt,.md,.csv,.json,.js,.ts,.jsx,.tsx,.html,.css,.py,.java,.rs,.go,.yaml,.yml,.xml,.toml,.sql,.sh" hidden></div>
|
||||
</div>
|
||||
<script>
|
||||
try {
|
||||
const vscode=acquireVsCodeApi(),chat=document.getElementById('chat'),input=document.getElementById('input'),
|
||||
sendBtn=document.getElementById('sendBtn'),stopBtn=document.getElementById('stopBtn'),
|
||||
modelSel=document.getElementById('modelSel'),brainSel=document.getElementById('brainSel'),refreshModelsBtn=document.getElementById('refreshModelsBtn'),manageBrainsBtn=document.getElementById('manageBrainsBtn'),newChatBtn=document.getElementById('newChatBtn'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
|
||||
historyBtn=document.getElementById('historyBtn'),historyOverlay=document.getElementById('historyOverlay'),closeHistoryBtn=document.getElementById('closeHistoryBtn'),historyList=document.getElementById('historyList'),
|
||||
internetBtn=document.getElementById('internetBtn'),attachBtn=document.getElementById('attachBtn'),fileInput=document.getElementById('fileInput'),attachPreview=document.getElementById('attachPreview'),
|
||||
thinkingBar=document.getElementById('thinkingBar');
|
||||
let loader=null,sending=false,pendingFiles=[],internetEnabled=false;
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>G1nation</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--bg-secondary: #0d1117;
|
||||
--surface: #161b22;
|
||||
--border: #30363d;
|
||||
--border-bright: #484f58;
|
||||
--text-primary: #c9d1d9;
|
||||
--text-bright: #ffffff;
|
||||
--text-dim: #8b949e;
|
||||
--accent: #58a6ff;
|
||||
--accent-glow: rgba(88, 166, 255, 0.15);
|
||||
--success: #238636;
|
||||
--warning: #d29922;
|
||||
--error: #f85149;
|
||||
--code-bg: #161b22;
|
||||
--table-header-bg: #161b22;
|
||||
--table-row-hover: #21262d;
|
||||
--input-bg: #0d1117;
|
||||
}
|
||||
|
||||
historyBtn.addEventListener('click', ()=>historyOverlay.classList.add('visible'));
|
||||
closeHistoryBtn.addEventListener('click', ()=>historyOverlay.classList.remove('visible'));
|
||||
refreshModelsBtn.addEventListener('click', ()=>vscode.postMessage({type:'refreshModels'}));
|
||||
manageBrainsBtn.addEventListener('click', ()=>vscode.postMessage({type:'manageBrains'}));
|
||||
brainSel.addEventListener('change', ()=>vscode.postMessage({type:'setBrainProfile',id:brainSel.value}));
|
||||
body.vscode-light {
|
||||
--bg: #ffffff;
|
||||
--bg-secondary: #f6f8fa;
|
||||
--surface: #ffffff;
|
||||
--border: #d0d7de;
|
||||
--border-bright: #afb8c1;
|
||||
--text-primary: #24292f;
|
||||
--text-bright: #111118;
|
||||
--text-dim: #57606a;
|
||||
--accent: #0969da;
|
||||
--accent-glow: rgba(9, 105, 218, 0.1);
|
||||
--success: #1a7f37;
|
||||
--warning: #9a6700;
|
||||
--error: #cf222e;
|
||||
--code-bg: #f6f8fa;
|
||||
--table-header-bg: #f6f8fa;
|
||||
--table-row-hover: #f3f4f6;
|
||||
--input-bg: #ffffff;
|
||||
}
|
||||
|
||||
internetBtn.addEventListener('click', ()=>{
|
||||
internetEnabled=!internetEnabled;
|
||||
internetBtn.style.opacity=internetEnabled?'1':'0.4';
|
||||
internetBtn.style.filter=internetEnabled?'none':'grayscale(1)';
|
||||
});
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
background: var(--bg);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
vscode.postMessage({type:'getModels'});
|
||||
setTimeout(()=>vscode.postMessage({type:'ready'}),300);
|
||||
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,150)+'px'});
|
||||
function getTime(){return new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'})}
|
||||
function esc(s){const d=document.createElement('div');d.innerText=s;return d.innerHTML}
|
||||
/* --- Header --- */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
function fmt(t){
|
||||
let formatted = esc(t);
|
||||
formatted = formatted.replace(/\`\`\`([\s\S]*?)\`\`\`/g, '<pre><code>$1</code></pre>');
|
||||
formatted = formatted.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
||||
return formatted;
|
||||
}
|
||||
.header-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
function normalizeMsgContent(content){
|
||||
if(Array.isArray(content)) return content.map(part=>typeof part==='string'?part:(part?.text||part?.name||JSON.stringify(part))).join('\\n');
|
||||
return String(content||'');
|
||||
}
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 14px 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
function resetChatView(){
|
||||
chat.innerHTML='';
|
||||
streamBody=null;hideLoader();setSending(false);
|
||||
document.body.classList.remove('init');
|
||||
}
|
||||
.brand { font-weight: 700; font-size: 14px; color: var(--text-bright); letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px; }
|
||||
.logo { width: 22px; height: 22px; background: var(--accent); color: #fff; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 900; }
|
||||
|
||||
function renderHistory(history){
|
||||
resetChatView();
|
||||
(history||[]).filter(m => !m.internal).forEach(m => addMsg(normalizeMsgContent(m.content), m.role === 'assistant' ? 'ai' : 'user'));
|
||||
if((history||[]).length===0){
|
||||
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">No messages in this session.</div></div>';
|
||||
document.body.classList.add('init');
|
||||
}
|
||||
chat.scrollTop=chat.scrollHeight;
|
||||
}
|
||||
.chat {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
function addMsg(text,role){
|
||||
const isUser=role==='user',isErr=role==='error';
|
||||
const el=document.createElement('div');el.className='msg'+(isUser?' msg-user':'')+(isErr?' msg-error':'');
|
||||
const head=document.createElement('div');head.className='msg-head';
|
||||
head.innerHTML=(isUser?'<div class="av av-user">👤</div><span>You</span>':'<div class="av av-ai">✦</div><span>G1nation</span>')+'<span class="msg-time">'+getTime()+'</span>';
|
||||
const body=document.createElement('div');body.className='msg-body';
|
||||
if(isUser){body.innerText=text}else{body.innerHTML=fmt(text)}
|
||||
el.appendChild(head);el.appendChild(body);chat.appendChild(el);chat.scrollTop=chat.scrollHeight;
|
||||
}
|
||||
/* --- Messages --- */
|
||||
.msg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
animation: msgIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
function showLoader(){thinkingBar.classList.add('active')}
|
||||
function hideLoader(){thinkingBar.classList.remove('active')}
|
||||
function setSending(v){sending=v;sendBtn.disabled=v;stopBtn.classList.toggle('visible',v);input.disabled=v;}
|
||||
@keyframes msgIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
function send(){
|
||||
const text=input.value.trim();
|
||||
if(!text && pendingFiles.length===0) return;
|
||||
document.body.classList.remove('init');
|
||||
const w=document.querySelector('.welcome');if(w)w.remove();
|
||||
addMsg(text,'user');
|
||||
input.value='';input.style.height='auto';setSending(true);showLoader();
|
||||
vscode.postMessage({type:'prompt',value:text,model:modelSel.value,internet:internetEnabled,files:pendingFiles.length?pendingFiles:undefined});
|
||||
pendingFiles=[];renderPreview();
|
||||
}
|
||||
.msg-head { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 11px; color: var(--text-dim); }
|
||||
.av { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; }
|
||||
.av-user { background: var(--border); color: var(--text-primary); }
|
||||
.av-ai { background: var(--accent); color: #fff; }
|
||||
|
||||
attachBtn.addEventListener('click',()=>fileInput.click());
|
||||
fileInput.addEventListener('change',()=>{
|
||||
const files=Array.from(fileInput.files);
|
||||
files.forEach(file=>{
|
||||
const reader=new FileReader();
|
||||
reader.onload=()=>{
|
||||
const base64=reader.result.split(',')[1];
|
||||
pendingFiles.push({name:file.name,type:file.type,data:base64});
|
||||
renderPreview();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
});
|
||||
.msg-body {
|
||||
padding-left: 30px;
|
||||
color: var(--text-primary);
|
||||
font-size: 13.5px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
function renderPreview(){
|
||||
attachPreview.innerHTML='';
|
||||
if(pendingFiles.length===0){attachPreview.classList.remove('visible');return;}
|
||||
attachPreview.classList.add('visible');
|
||||
pendingFiles.forEach((f,i)=>{
|
||||
const chip=document.createElement('div');chip.className='attach-chip';
|
||||
chip.innerHTML='<span>📎</span><span class="chip-name">'+f.name+'</span><span class="chip-remove">✕</span>';
|
||||
chip.querySelector('.chip-remove').addEventListener('click',()=>{pendingFiles.splice(i,1);renderPreview();});
|
||||
attachPreview.appendChild(chip);
|
||||
});
|
||||
}
|
||||
.msg-user .msg-body {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
margin-left: 30px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
sendBtn.addEventListener('click',send);
|
||||
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
|
||||
newChatBtn.addEventListener('click',()=>vscode.postMessage({type:'newChat'}));
|
||||
settingsBtn.addEventListener('click',()=>vscode.postMessage({type:'openSettings'}));
|
||||
brainBtn.addEventListener('click',()=>vscode.postMessage({type:'syncBrain'}));
|
||||
stopBtn.addEventListener('click',()=>{vscode.postMessage({type:'stopGeneration'});hideLoader();setSending(false);});
|
||||
/* --- Markdown Style --- */
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
|
||||
color: var(--text-bright);
|
||||
margin: 1.5em 0 0.8em;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
let streamBody=null,sessionCache={};
|
||||
window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
||||
case 'restoreHistory':
|
||||
renderHistory(msg.value||[]);
|
||||
break;
|
||||
case 'sessionLoaded':
|
||||
renderHistory(msg.value.history||[]);
|
||||
historyOverlay.classList.remove('visible');
|
||||
break;
|
||||
case 'sessionList':
|
||||
historyList.innerHTML='';
|
||||
sessionCache={};
|
||||
if(!msg.value || msg.value.length===0){
|
||||
historyList.innerHTML='<div class="history-item"><div class="history-item-info"><div class="history-item-title">No saved chats yet</div><div class="history-item-date">Start a conversation and it will appear here.</div></div></div>';
|
||||
break;
|
||||
}
|
||||
msg.value.forEach(s=>{
|
||||
sessionCache[s.id]=s;
|
||||
const el=document.createElement('div');el.className='history-item';
|
||||
el.dataset.sessionId=s.id;
|
||||
el.innerHTML='<div class="history-item-info"><div class="history-item-title">'+esc(s.title)+'</div><div class="history-item-date">'+new Date(s.timestamp).toLocaleString()+' · '+(s.messageCount||0)+' messages</div></div><button class="history-del-btn" title="Delete">🗑</button>';
|
||||
el.addEventListener('click',(e)=>{
|
||||
if(e.target.closest('.history-del-btn')) return;
|
||||
const cached=sessionCache[s.id];
|
||||
if(cached && cached.history){renderHistory(cached.history);historyOverlay.classList.remove('visible');}
|
||||
vscode.postMessage({type:'loadSession',id:s.id});
|
||||
.markdown-body h1 { font-size: 1.5em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; color: var(--accent); }
|
||||
.markdown-body h2 { font-size: 1.3em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
|
||||
.markdown-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.2em 0;
|
||||
font-size: 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown-body th { background: var(--table-header-bg); color: var(--accent); font-weight: 600; text-align: left; padding: 10px 12px; border: 1px solid var(--border); }
|
||||
.markdown-body td { padding: 8px 12px; border: 1px solid var(--border); color: var(--text-primary); }
|
||||
.markdown-body tr:nth-child(even) { background: rgba(255, 255, 255, 0.02); }
|
||||
|
||||
.markdown-body pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px; overflow-x: auto; margin: 12px 0; }
|
||||
.markdown-body code { font-family: 'SF Mono', monospace; font-size: 11.5px; background: rgba(175, 184, 193, 0.2); padding: 0.2em 0.4em; border-radius: 4px; }
|
||||
|
||||
/* --- UI Elements --- */
|
||||
.copy-btn {
|
||||
position: absolute; top: 0; right: 0; background: var(--bg-secondary); border: 1px solid var(--border);
|
||||
color: var(--text-dim); padding: 4px 10px; border-radius: 6px; font-size: 10px; cursor: pointer; opacity: 0; transition: 0.2s;
|
||||
z-index: 20;
|
||||
}
|
||||
.msg:hover .copy-btn { opacity: 1; }
|
||||
.copy-btn:hover { color: var(--text-bright); border-color: var(--accent); background: var(--accent-glow); }
|
||||
|
||||
.icon-btn {
|
||||
background: var(--surface); border: 1px solid var(--border); color: var(--text-dim); width: 28px; height: 28px;
|
||||
border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; font-size: 13px;
|
||||
}
|
||||
.icon-btn:hover { color: var(--text-bright); border-color: var(--accent); background: var(--accent-glow); }
|
||||
.icon-btn.active { color: var(--accent); border-color: var(--accent); background: var(--accent-glow); }
|
||||
|
||||
select { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); padding: 4px 8px; border-radius: 6px; font-size: 10.5px; cursor: pointer; }
|
||||
|
||||
/* --- Input & Attachments --- */
|
||||
.input-wrap {
|
||||
padding: 12px 14px 16px; background: var(--bg); border-top: 1px solid var(--border); flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
background: var(--input-bg); border: 1px solid var(--border); border-radius: 12px; padding: 10px 14px;
|
||||
display: flex; flex-direction: column; gap: 8px; transition: 0.2s;
|
||||
}
|
||||
.input-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
|
||||
|
||||
textarea {
|
||||
width: 100%; background: transparent; border: none; color: var(--text-bright);
|
||||
font-family: inherit; font-size: 13.5px; resize: none; outline: none; min-height: 24px; max-height: 160px;
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
display: none; gap: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--border); flex-wrap: wrap;
|
||||
}
|
||||
.attachment-preview.visible { display: flex; }
|
||||
|
||||
.file-chip {
|
||||
display: flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--border);
|
||||
padding: 4px 8px; border-radius: 6px; font-size: 11px; color: var(--text-primary);
|
||||
}
|
||||
.file-chip .remove { cursor: pointer; color: var(--text-dim); }
|
||||
.file-chip .remove:hover { color: var(--error); }
|
||||
|
||||
.input-footer { display: flex; align-items: center; justify-content: space-between; }
|
||||
.footer-left { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
.send-btn {
|
||||
background: var(--accent); color: #fff; border: none; padding: 6px 14px; border-radius: 6px;
|
||||
font-weight: 600; font-size: 12px; cursor: pointer;
|
||||
}
|
||||
.send-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
|
||||
/* --- Overlays & Others --- */
|
||||
.thinking-bar { height: 2px; background: transparent; position: relative; overflow: hidden; margin-top: -1px; }
|
||||
.thinking-bar.active::after {
|
||||
content: ''; position: absolute; top: 0; left: -40%; width: 40%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, var(--accent), transparent); animation: think 1.5s infinite;
|
||||
}
|
||||
@keyframes think { 0% { left: -40%; } 100% { left: 100%; } }
|
||||
|
||||
.history-overlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8);
|
||||
backdrop-filter: blur(10px); z-index: 1000; display: none; flex-direction: column; padding: 20px;
|
||||
}
|
||||
.history-overlay.visible { display: flex; }
|
||||
|
||||
.stream-active::after {
|
||||
content: ''; display: inline-block; width: 6px; height: 14px; background: var(--accent);
|
||||
margin-left: 4px; animation: blink 0.8s step-end infinite; vertical-align: middle;
|
||||
}
|
||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
||||
|
||||
.welcome { text-align: center; padding: 40px 20px; color: var(--text-dim); }
|
||||
.welcome-logo { font-size: 48px; color: var(--accent); margin-bottom: 16px; opacity: 0.8; }
|
||||
.welcome-title { font-size: 20px; font-weight: 700; color: var(--text-bright); margin-bottom: 8px; }
|
||||
|
||||
/* --- History List --- */
|
||||
.history-item {
|
||||
padding: 12px; border-radius: 8px; background: var(--surface); border: 1px solid var(--border);
|
||||
margin-bottom: 10px; cursor: pointer; transition: 0.2s;
|
||||
}
|
||||
.history-item:hover { border-color: var(--accent); background: var(--accent-glow); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-top">
|
||||
<div class="brand"><div class="logo">✦</div> G1nation</div>
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<button class="icon-btn" id="newChatBtn" title="New Chat">+</button>
|
||||
<button class="icon-btn" id="settingsBtn" title="Settings">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<div id="statusDot" style="width:6px; height:6px; border-radius:50%; background:var(--text-dim);"></div>
|
||||
<select id="modelSel" title="Select Model"></select>
|
||||
<select id="brainSel" title="Select Brain"></select>
|
||||
<button class="icon-btn" id="internetBtn" title="Internet Access">🌐</button>
|
||||
<button class="icon-btn" id="brainBtn" title="Sync Knowledge">🧠</button>
|
||||
<button class="icon-btn" id="historyBtn" title="History">📜</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="historyOverlay" class="history-overlay">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h2 style="color:var(--text-bright);">Chat History</h2>
|
||||
<button class="icon-btn" id="closeHistoryBtn">✕</button>
|
||||
</div>
|
||||
<div id="historyList" style="flex:1; overflow-y:auto;"></div>
|
||||
</div>
|
||||
|
||||
<div class="thinking-bar" id="thinkingBar"></div>
|
||||
|
||||
<div class="chat" id="chat">
|
||||
<div class="welcome">
|
||||
<div class="welcome-logo">✦</div>
|
||||
<div class="welcome-title">Welcome to G1nation</div>
|
||||
<p>Your premium local AI assistant.<br>Ready to analyze projects and build reports.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<div class="input-box">
|
||||
<div id="attachPreview" class="attachment-preview"></div>
|
||||
<textarea id="input" rows="1" placeholder="Type your request..."></textarea>
|
||||
<div class="input-footer">
|
||||
<div class="footer-left">
|
||||
<button class="icon-btn" id="attachBtn" title="Attach Files">📎</button>
|
||||
<span id="statusLabel" style="font-size:10px; color:var(--text-dim);">Ready</span>
|
||||
</div>
|
||||
<button id="sendBtn" class="send-btn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="fileInput" multiple hidden accept="image/*,.txt,.md,.pdf,.csv,.json,.js,.ts,.py,.java,.rs,.go">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
const chat = document.getElementById('chat');
|
||||
const input = document.getElementById('input');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const thinkingBar = document.getElementById('thinkingBar');
|
||||
const statusLabel = document.getElementById('statusLabel');
|
||||
const modelSel = document.getElementById('modelSel');
|
||||
const brainSel = document.getElementById('brainSel');
|
||||
const historyOverlay = document.getElementById('historyOverlay');
|
||||
const historyList = document.getElementById('historyList');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const attachBtn = document.getElementById('attachBtn');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const attachPreview = document.getElementById('attachPreview');
|
||||
|
||||
let streamBody = null;
|
||||
let internetEnabled = false;
|
||||
let pendingFiles = [];
|
||||
|
||||
function fmt(text) { return marked.parse(text || ''); }
|
||||
|
||||
function copyToClipboard(text, btn) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea); textarea.select();
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
btn.innerText = '✅ Copied!'; setTimeout(() => { btn.innerText = '📋 Copy'; }, 2000);
|
||||
}
|
||||
} catch (err) { console.error('Copy failed', err); }
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
function addMsg(text, role) {
|
||||
const isUser = role === 'user';
|
||||
const msgEl = document.createElement('div');
|
||||
msgEl.className = 'msg ' + (isUser ? 'msg-user' : 'msg-ai');
|
||||
msgEl._raw = text;
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'msg-head';
|
||||
head.innerHTML = isUser ? '<div class="av av-user">U</div> You' : '<div class="av av-ai">✦</div> G1nation';
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'msg-body markdown-body';
|
||||
|
||||
if (isUser) {
|
||||
body.innerText = text;
|
||||
} else {
|
||||
body.innerHTML = fmt(text);
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'copy-btn'; copyBtn.innerText = '📋 Copy';
|
||||
copyBtn.onclick = (e) => { e.stopPropagation(); copyToClipboard(msgEl._raw, copyBtn); };
|
||||
msgEl.appendChild(copyBtn);
|
||||
}
|
||||
|
||||
msgEl.appendChild(head); msgEl.appendChild(body);
|
||||
chat.appendChild(msgEl); chat.scrollTop = chat.scrollHeight;
|
||||
return { body, msgEl };
|
||||
}
|
||||
|
||||
window.addEventListener('message', e => {
|
||||
const msg = e.data;
|
||||
switch(msg.type) {
|
||||
case 'streamStart':
|
||||
thinkingBar.classList.remove('active');
|
||||
if (document.querySelector('.welcome')) document.querySelector('.welcome').remove();
|
||||
const res = addMsg('', 'assistant');
|
||||
streamBody = res.body; streamBody._parent = res.msgEl; streamBody._parent._raw = '';
|
||||
streamBody.classList.add('stream-active');
|
||||
break;
|
||||
case 'streamChunk':
|
||||
if (streamBody) {
|
||||
streamBody._parent._raw += msg.value;
|
||||
streamBody.innerHTML = fmt(streamBody._parent._raw);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
break;
|
||||
case 'streamEnd':
|
||||
if (streamBody) streamBody.classList.remove('stream-active');
|
||||
streamBody = null; sendBtn.disabled = false;
|
||||
break;
|
||||
case 'restoreHistory':
|
||||
case 'sessionLoaded':
|
||||
const history = msg.type === 'sessionLoaded' ? msg.value.history : msg.value;
|
||||
if (history && history.length > 0) {
|
||||
chat.innerHTML = '';
|
||||
history.forEach(m => addMsg(m.content, m.role === 'assistant' ? 'assistant' : 'user'));
|
||||
}
|
||||
historyOverlay.classList.remove('visible');
|
||||
break;
|
||||
case 'clearChat':
|
||||
chat.innerHTML = '<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">Welcome to G1nation</div><p>Your premium local AI assistant.<br>Ready to analyze projects and build reports.</p></div>';
|
||||
break;
|
||||
case 'focusInput':
|
||||
input.focus();
|
||||
break;
|
||||
case 'modelsList':
|
||||
modelSel.innerHTML = '';
|
||||
msg.value.models.forEach(m => {
|
||||
const o = document.createElement('option'); o.value = m; o.innerText = m;
|
||||
if (m === msg.value.selected) o.selected = true;
|
||||
modelSel.appendChild(o);
|
||||
});
|
||||
statusLabel.innerText = \`Model: \${msg.value.selected}\`;
|
||||
break;
|
||||
case 'brainProfiles':
|
||||
brainSel.innerHTML = '';
|
||||
msg.value.profiles.forEach(p => {
|
||||
const o = document.createElement('option'); o.value = p.id; o.innerText = p.name;
|
||||
if (p.id === msg.value.activeBrainId) o.selected = true;
|
||||
brainSel.appendChild(o);
|
||||
});
|
||||
const addOpt = document.createElement('option');
|
||||
addOpt.value = 'new'; addOpt.innerText = '+ Add New Brain...';
|
||||
brainSel.appendChild(addOpt);
|
||||
break;
|
||||
case 'sessionList':
|
||||
historyList.innerHTML = '';
|
||||
msg.value.forEach(s => {
|
||||
const el = document.createElement('div'); el.className = 'history-item';
|
||||
el.innerHTML = \`<div style="font-weight:600; color:var(--text-bright); mb-2">\${s.title}</div><div style="font-size:10px; color:var(--text-dim)">\${new Date(s.timestamp).toLocaleString()} · \${s.messageCount} msgs</div>\`;
|
||||
el.onclick = () => vscode.postMessage({ type: 'loadSession', id: s.id });
|
||||
historyList.appendChild(el);
|
||||
});
|
||||
break;
|
||||
case 'engineStatus':
|
||||
statusDot.style.background = msg.value.online ? 'var(--success)' : 'var(--error)';
|
||||
break;
|
||||
case 'autoContinue':
|
||||
statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
|
||||
setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
|
||||
break;
|
||||
case 'error':
|
||||
thinkingBar.classList.remove('active'); sendBtn.disabled = false;
|
||||
addMsg(msg.value, 'error');
|
||||
break;
|
||||
}
|
||||
});
|
||||
el.querySelector('.history-del-btn').addEventListener('click',()=>{
|
||||
vscode.postMessage({type:'deleteSession',id:s.id});
|
||||
|
||||
function renderAttachments() {
|
||||
attachPreview.innerHTML = '';
|
||||
if (pendingFiles.length === 0) { attachPreview.classList.remove('visible'); return; }
|
||||
attachPreview.classList.add('visible');
|
||||
pendingFiles.forEach((f, i) => {
|
||||
const chip = document.createElement('div'); chip.className = 'file-chip';
|
||||
chip.innerHTML = \`<span>📎</span> \${f.name} <span class="remove" onclick="removeFile(\${i})">✕</span>\`;
|
||||
attachPreview.appendChild(chip);
|
||||
});
|
||||
}
|
||||
window.removeFile = (i) => { pendingFiles.splice(i, 1); renderAttachments(); };
|
||||
attachBtn.onclick = () => fileInput.click();
|
||||
fileInput.onchange = () => {
|
||||
Array.from(fileInput.files).forEach(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result.split(',')[1];
|
||||
pendingFiles.push({ name: file.name, type: file.type, data: base64 });
|
||||
renderAttachments();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
fileInput.value = '';
|
||||
};
|
||||
|
||||
function send() {
|
||||
const val = input.value.trim();
|
||||
if (!val && pendingFiles.length === 0) return;
|
||||
addMsg(val || (pendingFiles.length > 0 ? \`[Sent \${pendingFiles.length} files]\` : ''), 'user');
|
||||
vscode.postMessage({ type: 'prompt', value: val, model: modelSel.value, internet: internetEnabled, files: pendingFiles.length > 0 ? pendingFiles : undefined });
|
||||
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
|
||||
sendBtn.disabled = true; thinkingBar.classList.add('active');
|
||||
}
|
||||
|
||||
sendBtn.onclick = send;
|
||||
input.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.isComposing) return;
|
||||
e.preventDefault();
|
||||
send();
|
||||
}
|
||||
});
|
||||
historyList.appendChild(el);
|
||||
});
|
||||
break;
|
||||
case 'streamStart':
|
||||
hideLoader();
|
||||
const el=document.createElement('div');el.className='msg';
|
||||
el.innerHTML='<div class="msg-head"><div class="av av-ai">✦</div><span>G1nation</span><span class="msg-time">'+getTime()+'</span></div><div class="msg-body stream-active"></div>';
|
||||
chat.appendChild(el);streamBody=el.querySelector('.msg-body');chat.scrollTop=chat.scrollHeight;
|
||||
break;
|
||||
case 'streamChunk':
|
||||
if(streamBody){streamBody.innerHTML=fmt(streamBody._raw=(streamBody._raw||'')+msg.value);chat.scrollTop=chat.scrollHeight;}
|
||||
break;
|
||||
case 'streamEnd':
|
||||
if(streamBody)streamBody.classList.remove('stream-active');
|
||||
setSending(false);streamBody=null;
|
||||
break;
|
||||
case 'modelsList':
|
||||
modelSel.innerHTML='';
|
||||
(msg.value.models||[]).forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});
|
||||
if(msg.value.selected) modelSel.value=msg.value.selected;
|
||||
break;
|
||||
case 'brainProfiles':
|
||||
brainSel.innerHTML='';
|
||||
(msg.value.profiles||[]).forEach(profile=>{
|
||||
const o=document.createElement('option');o.value=profile.id;o.textContent=profile.name;brainSel.appendChild(o);
|
||||
o.title=profile.path;
|
||||
});
|
||||
if(msg.value.activeBrainId) brainSel.value=msg.value.activeBrainId;
|
||||
break;
|
||||
case 'engineStatus':
|
||||
const dot = document.getElementById('engineStatusDot');
|
||||
if(dot){
|
||||
dot.style.background = msg.value.online ? 'var(--accent)' : 'var(--red)';
|
||||
dot.title = msg.value.online ? \`AI Engine Online (\${msg.value.url})\` : \`AI Engine Offline (Check \${msg.value.url})\`;
|
||||
}
|
||||
break;
|
||||
case 'brainStatus':
|
||||
const badge = document.getElementById('brainCountBadge');
|
||||
const info = document.getElementById('brainStatusInfo');
|
||||
const meta = document.getElementById('brainStatusMeta');
|
||||
if(badge){
|
||||
badge.innerText = msg.value.count > 999 ? '999+' : msg.value.count;
|
||||
badge.style.display = msg.value.count > 0 ? 'block' : 'none';
|
||||
}
|
||||
if(info){
|
||||
info.innerText = \`Brain: \${msg.value.name} · \${msg.value.count} files\`;
|
||||
info.title = msg.value.path;
|
||||
}
|
||||
if(meta){
|
||||
meta.innerText = msg.value.description ? \`\${msg.value.description} · \${msg.value.path}\` : msg.value.path;
|
||||
meta.title = msg.value.path;
|
||||
}
|
||||
const syncBrainBtn = document.getElementById('brainBtn');
|
||||
if(syncBrainBtn) syncBrainBtn.title = \`Sync Brain: \${msg.value.name} (\${msg.value.count} files at \${msg.value.path})\`;
|
||||
break;
|
||||
case 'clearChat':
|
||||
streamBody=null;hideLoader();setSending(false);
|
||||
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">System Cleaned.</div></div>';
|
||||
document.body.classList.add('init');
|
||||
break;
|
||||
case 'injectPrompt':
|
||||
input.value=msg.value;send();
|
||||
break;
|
||||
case 'autoContinue':
|
||||
showLoader();setSending(true);
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'auto-status';
|
||||
hint.innerText = msg.value;
|
||||
chat.appendChild(hint);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
break;
|
||||
case 'error':
|
||||
hideLoader();setSending(false);addMsg('Error: '+msg.value,'error');
|
||||
break;
|
||||
} });
|
||||
} catch(err) { console.error(err); }
|
||||
</script></body></html>`;
|
||||
input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px'; });
|
||||
|
||||
document.getElementById('newChatBtn').onclick = () => vscode.postMessage({ type: 'newChat' });
|
||||
document.getElementById('settingsBtn').onclick = () => vscode.postMessage({ type: 'openSettings' });
|
||||
document.getElementById('internetBtn').onclick = () => {
|
||||
internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled);
|
||||
};
|
||||
document.getElementById('brainBtn').onclick = () => vscode.postMessage({ type: 'syncBrain' });
|
||||
document.getElementById('historyBtn').onclick = () => vscode.postMessage({ type: 'getSessions' });
|
||||
document.getElementById('historyBtn').addEventListener('click', () => historyOverlay.classList.add('visible'));
|
||||
document.getElementById('closeHistoryBtn').onclick = () => historyOverlay.classList.remove('visible');
|
||||
modelSel.onchange = () => vscode.postMessage({ type: 'refreshModels' });
|
||||
brainSel.onchange = () => {
|
||||
if (brainSel.value === 'new') {
|
||||
vscode.postMessage({ type: 'addBrain' });
|
||||
} else {
|
||||
vscode.postMessage({ type: 'setBrainProfile', id: brainSel.value });
|
||||
}
|
||||
};
|
||||
|
||||
vscode.postMessage({ type: 'getModels' });
|
||||
vscode.postMessage({ type: 'ready' });
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user