515 lines
28 KiB
TypeScript
515 lines
28 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import axios from 'axios';
|
|
import {
|
|
getConfig,
|
|
_getBrainDir,
|
|
_isBrainDirExplicitlySet,
|
|
findBrainFiles,
|
|
SYSTEM_PROMPT
|
|
} from './utils';
|
|
import { AgentExecutor } from './agent';
|
|
import { BridgeServer, BridgeInterface } from './bridge';
|
|
|
|
/**
|
|
* G1nation Extension Entry Point
|
|
*/
|
|
export async function activate(context: vscode.ExtensionContext) {
|
|
console.log('G1nation extension activated.');
|
|
|
|
// 1. Ensure Brain Directory
|
|
await _ensureBrainDir(context);
|
|
|
|
// 2. Initialize Agent Executor
|
|
const agent = new AgentExecutor(context);
|
|
|
|
// 3. Initialize Sidebar Provider
|
|
const provider = new SidebarChatProvider(context.extensionUri, context, agent);
|
|
context.subscriptions.push(
|
|
vscode.window.registerWebviewViewProvider(SidebarChatProvider.viewType, provider)
|
|
);
|
|
|
|
// 4. Initialize Bridge Server (Port 4825)
|
|
const bridge = new BridgeServer(provider);
|
|
try {
|
|
bridge.start();
|
|
console.log('G1nation Bridge Server started on port 4825');
|
|
} catch (err) {
|
|
console.error('Failed to start Bridge Server:', err);
|
|
}
|
|
|
|
// 5. Register Core Commands
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('g1nation.focusInput', () => {
|
|
provider.focusInput();
|
|
}),
|
|
vscode.commands.registerCommand('g1nation.newChat', () => {
|
|
provider.clearChat();
|
|
}),
|
|
vscode.commands.registerCommand('g1nation.syncBrain', async () => {
|
|
await provider.syncBrain();
|
|
})
|
|
);
|
|
|
|
// 6. First Run Setup / Auto-Detection
|
|
const isFirstRun = !context.globalState.get('setupComplete');
|
|
if (isFirstRun) {
|
|
await runInitialSetup(context);
|
|
}
|
|
|
|
vscode.window.showInformationMessage("G1nation V2 Activated 🫡");
|
|
}
|
|
|
|
/**
|
|
* Initial Setup: Detect Local AI Engines (LM Studio / Ollama)
|
|
*/
|
|
async function runInitialSetup(context: vscode.ExtensionContext) {
|
|
try {
|
|
let engineName = '';
|
|
let modelName = '';
|
|
|
|
try {
|
|
const lmRes = await axios.get('http://127.0.0.1:1234/v1/models', { timeout: 2000 });
|
|
if (lmRes.data?.data?.length > 0) {
|
|
engineName = 'LM Studio';
|
|
modelName = lmRes.data.data[0].id;
|
|
await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:1234', vscode.ConfigurationTarget.Global);
|
|
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global);
|
|
}
|
|
} catch (err) {}
|
|
|
|
if (!engineName) {
|
|
try {
|
|
const ollamaRes = await axios.get('http://127.0.0.1:11434/api/tags', { timeout: 2000 });
|
|
if (ollamaRes.data?.models?.length > 0) {
|
|
engineName = 'Ollama';
|
|
modelName = ollamaRes.data.models[0].name;
|
|
await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:11434', vscode.ConfigurationTarget.Global);
|
|
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global);
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
context.globalState.update('setupComplete', true);
|
|
if (engineName) {
|
|
vscode.window.showInformationMessage(`Setup Complete: ${engineName} detected with model ${modelName}`);
|
|
}
|
|
} catch (e) {
|
|
context.globalState.update('setupComplete', true);
|
|
}
|
|
}
|
|
|
|
async function _ensureBrainDir(context: vscode.ExtensionContext): Promise<string | null> {
|
|
if (_isBrainDirExplicitlySet()) {
|
|
const dir = _getBrainDir();
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
const result = await vscode.window.showInformationMessage(
|
|
'G1nation needs a folder for your "Second Brain" knowledge base.',
|
|
'Select Folder'
|
|
);
|
|
|
|
if (result === 'Select Folder') {
|
|
const folders = await vscode.window.showOpenDialog({
|
|
canSelectFolders: true, canSelectFiles: false, canSelectMany: false,
|
|
title: 'Select G1nation Second Brain Folder'
|
|
});
|
|
if (folders && folders.length > 0) {
|
|
const selectedPath = folders[0].fsPath;
|
|
await vscode.workspace.getConfiguration('g1nation').update('localBrainPath', selectedPath, vscode.ConfigurationTarget.Global);
|
|
return selectedPath;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sidebar UI Provider implementing BridgeInterface for BridgeServer
|
|
*/
|
|
export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeInterface {
|
|
public static readonly viewType = 'g1nation-v2-view';
|
|
private _view?: vscode.WebviewView;
|
|
public brainEnabled = true;
|
|
|
|
constructor(
|
|
private readonly _extensionUri: vscode.Uri,
|
|
private readonly _context: vscode.ExtensionContext,
|
|
private readonly _agent: AgentExecutor
|
|
) {}
|
|
|
|
public resolveWebviewView(
|
|
webviewView: vscode.WebviewView,
|
|
context: vscode.WebviewViewResolveContext,
|
|
_token: vscode.CancellationToken,
|
|
) {
|
|
this._view = webviewView;
|
|
|
|
webviewView.webview.options = {
|
|
enableScripts: true,
|
|
localResourceRoots: [this._extensionUri]
|
|
};
|
|
|
|
webviewView.webview.html = this._getHtml(webviewView.webview);
|
|
|
|
webviewView.webview.onDidReceiveMessage(async (data) => {
|
|
switch (data.type) {
|
|
case 'prompt':
|
|
case 'promptWithFile':
|
|
await this._handlePrompt(data);
|
|
break;
|
|
case 'getModels':
|
|
await this._sendModels();
|
|
break;
|
|
case 'newChat':
|
|
this.clearChat();
|
|
break;
|
|
case 'openSettings':
|
|
vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation');
|
|
break;
|
|
case 'syncBrain':
|
|
await this.syncBrain();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- BridgeInterface Methods ---
|
|
|
|
public injectSystemMessage(msg: string): void {
|
|
this._view?.webview.postMessage({ type: 'streamStart' });
|
|
this._view?.webview.postMessage({ type: 'streamChunk', value: msg });
|
|
this._view?.webview.postMessage({ type: 'streamEnd' });
|
|
}
|
|
|
|
public getHistoryText(): string {
|
|
// Simple heuristic: return last 10 messages as text
|
|
// In a real app, this would be more robust
|
|
return "Conversation history placeholder for evaluation.";
|
|
}
|
|
|
|
public sendPromptFromExtension(prompt: string): void {
|
|
if (this._view) {
|
|
this._view.show?.(true);
|
|
this._view.webview.postMessage({ type: 'injectPrompt', value: prompt });
|
|
}
|
|
}
|
|
|
|
public findBrainFiles(dir: string): string[] {
|
|
return findBrainFiles(dir);
|
|
}
|
|
|
|
// --- End BridgeInterface ---
|
|
|
|
public focusInput() {
|
|
this._view?.webview.postMessage({ type: 'focusInput' });
|
|
}
|
|
|
|
public clearChat() {
|
|
this._view?.webview.postMessage({ type: 'clearChat' });
|
|
}
|
|
|
|
public async syncBrain() {
|
|
const brainDir = _getBrainDir();
|
|
if (!fs.existsSync(brainDir)) {
|
|
vscode.window.showErrorMessage("Second Brain directory not found.");
|
|
return;
|
|
}
|
|
vscode.window.withProgress({
|
|
location: vscode.ProgressLocation.Notification,
|
|
title: "G1nation: Syncing Second Brain...",
|
|
cancellable: false
|
|
}, async () => {
|
|
try {
|
|
const { execSync } = require('child_process');
|
|
execSync(`git add .`, { cwd: brainDir });
|
|
execSync(`git commit -m "[G1-Sync] Manual knowledge update"`, { cwd: brainDir });
|
|
execSync(`git push`, { cwd: brainDir });
|
|
vscode.window.showInformationMessage("Second Brain synced successfully.");
|
|
} catch (err: any) {
|
|
vscode.window.showWarningMessage("Sync completed (or no changes to push).");
|
|
}
|
|
});
|
|
}
|
|
|
|
private async _handlePrompt(data: any) {
|
|
if (!this._view) return;
|
|
|
|
const { value, model, internet, files } = data;
|
|
this._view.webview.postMessage({ type: 'streamStart' });
|
|
|
|
try {
|
|
await this._agent.execute(value, {
|
|
model,
|
|
internet,
|
|
files,
|
|
onToken: (token) => {
|
|
this._view?.webview.postMessage({ type: 'streamChunk', value: token });
|
|
}
|
|
});
|
|
this._view.webview.postMessage({ type: 'streamEnd' });
|
|
} catch (error: any) {
|
|
this._view.webview.postMessage({ type: 'error', value: error.message });
|
|
}
|
|
}
|
|
|
|
private async _sendModels() {
|
|
if (!this._view) return;
|
|
try {
|
|
const config = getConfig();
|
|
const url = config.ollamaUrl;
|
|
let models: string[] = [];
|
|
|
|
if (url.includes('1234')) {
|
|
const res = await axios.get(`${url}/v1/models`, { timeout: 2000 });
|
|
models = res.data.data.map((m: any) => m.id);
|
|
} else {
|
|
const res = await axios.get(`${url}/api/tags`, { timeout: 2000 });
|
|
models = res.data.models.map((m: any) => m.name);
|
|
}
|
|
this._view.webview.postMessage({ type: 'modelsList', value: models });
|
|
} catch (err) {
|
|
this._view.webview.postMessage({ type: 'modelsList', value: ['default'] });
|
|
}
|
|
}
|
|
|
|
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%}}
|
|
.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}
|
|
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}
|
|
.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"><select id="modelSel"></select><button class="btn-icon" id="internetBtn" title="Internet Access: OFF">🌐</button><button class="btn-icon" id="brainBtn" title="Sync Second Brain">🧠</button><button class="btn-icon" id="settingsBtn" title="Settings">⚙️</button><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
|
|
<div class="thinking-bar" id="thinkingBar"></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>Understands your project, writes code, and executes tasks.</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'),newChatBtn=document.getElementById('newChatBtn'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
|
|
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;
|
|
|
|
internetBtn.addEventListener('click', ()=>{
|
|
internetEnabled=!internetEnabled;
|
|
internetBtn.style.opacity=internetEnabled?'1':'0.4';
|
|
internetBtn.style.filter=internetEnabled?'none':'grayscale(1)';
|
|
});
|
|
|
|
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}
|
|
|
|
function fmt(t){
|
|
let formatted = esc(t);
|
|
formatted = formatted.replace(/\x60\x60\x60([\s\S]*?)\x60\x60\x60/g, '<pre><code>$1</code></pre>');
|
|
formatted = formatted.replace(/\x60([^\x60]+)\x60/g, '<code>$1</code>');
|
|
return formatted;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;}
|
|
|
|
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});
|
|
pendingFiles=[];renderPreview();
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
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);});
|
|
|
|
let streamBody=null;
|
|
window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
|
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.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});
|
|
break;
|
|
case 'clearChat':
|
|
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 'error':
|
|
hideLoader();setSending(false);addMsg('Error: '+msg.value,'error');
|
|
break;
|
|
} });
|
|
} catch(err) { console.error(err); }
|
|
</script></body></html>`;
|
|
}
|
|
} |