diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7df5b04 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run VS Code Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}/apps/vscode"], + "outFiles": ["${workspaceFolder}/apps/vscode/dist/**/*.js"], + "preLaunchTask": "build:vscode" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..af96e80 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build:vscode", + "type": "shell", + "command": "npm run build", + "options": { "cwd": "${workspaceFolder}/apps/vscode" }, + "group": { "kind": "build", "isDefault": true }, + "presentation": { "reveal": "always" }, + "problemMatcher": [] + } + ] +} diff --git a/apps/extension-webview/.gitignore b/apps/extension-webview/.gitignore new file mode 100644 index 0000000..9955061 --- /dev/null +++ b/apps/extension-webview/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +df/ diff --git a/apps/extension-webview/ai-agent.html b/apps/extension-webview/ai-agent.html new file mode 100644 index 0000000..f6be3af --- /dev/null +++ b/apps/extension-webview/ai-agent.html @@ -0,0 +1,12 @@ + + + + + + Vespertide AI Agent + + +
+ + + diff --git a/apps/extension-webview/index.html b/apps/extension-webview/index.html new file mode 100644 index 0000000..973f27f --- /dev/null +++ b/apps/extension-webview/index.html @@ -0,0 +1,12 @@ + + + + + + Vespertide + + +
+ + + diff --git a/apps/extension-webview/package.json b/apps/extension-webview/package.json new file mode 100644 index 0000000..3db3039 --- /dev/null +++ b/apps/extension-webview/package.json @@ -0,0 +1,24 @@ +{ + "name": "extension-webview", + "private": true, + "version": "0.1.61", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@devup-ui/react": "^1.0.36", + "@devup-ui/vite-plugin": "^1.0.59", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.3.0", + "vite": "^5.4.0" + } +} diff --git a/apps/extension-webview/src/AIAgentApp.tsx b/apps/extension-webview/src/AIAgentApp.tsx new file mode 100644 index 0000000..f780f7b --- /dev/null +++ b/apps/extension-webview/src/AIAgentApp.tsx @@ -0,0 +1,514 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { postMessage, onMessage } from './vscode'; +import type { ConnectorService, ConnectorStatus, ChatMessage } from './vscode'; + +type ConnectorMeta = { + service: ConnectorService; + label: string; + icon: string; + subtitle: string; + isAI: boolean; + keyLabel: string; + keyPlaceholder: string; + getKeyUrl: string; + getKeyLabel: string; + steps: string[]; +}; + +const CONNECTORS: ConnectorMeta[] = [ + { + service: 'claude', label: 'Claude', icon: 'πŸ€–', subtitle: 'Anthropic', isAI: true, + keyLabel: 'API Key', keyPlaceholder: 'sk-ant-api03-...', + getKeyUrl: 'https://console.anthropic.com/settings/keys', + getKeyLabel: 'API ν‚€ λ°œκΈ‰ β†’', + steps: ['console.anthropic.com 둜그인', 'Settings β†’ API Keys', 'Create Key 클릭 ν›„ 볡사'], + }, + { + service: 'openai', label: 'OpenAI / GPT', icon: '🧠', subtitle: 'OpenAI', isAI: true, + keyLabel: 'API Key', keyPlaceholder: 'sk-proj-...', + getKeyUrl: 'https://platform.openai.com/api-keys', + getKeyLabel: 'API ν‚€ λ°œκΈ‰ β†’', + steps: ['platform.openai.com 둜그인', 'API Keys 메뉴 선택', 'Create new secret key 클릭 ν›„ 볡사'], + }, + { + service: 'gemini', label: 'Gemini', icon: '✦', subtitle: 'Google', isAI: true, + keyLabel: 'API Key', keyPlaceholder: 'AIzaSy...', + getKeyUrl: 'https://aistudio.google.com/app/apikey', + getKeyLabel: 'API ν‚€ λ°œκΈ‰ β†’', + steps: ['aistudio.google.com λ°©λ¬Έ', 'Get API Key 클릭', 'Create API key ν›„ 볡사'], + }, + { + service: 'slack', label: 'Slack', icon: 'πŸ’¬', subtitle: 'Workspace', isAI: false, + keyLabel: 'Webhook URL', keyPlaceholder: 'https://hooks.slack.com/services/...', + getKeyUrl: 'https://api.slack.com/apps', + getKeyLabel: 'Webhook 생성 β†’', + steps: ['api.slack.com/apps β†’ Create App', 'Incoming Webhooks ν™œμ„±ν™”', 'Add New Webhook to Workspace', 'Webhook URL 볡사'], + }, + { + service: 'notion', label: 'Notion', icon: 'πŸ“', subtitle: 'Workspace', isAI: false, + keyLabel: 'Integration Token', keyPlaceholder: 'secret_...', + getKeyUrl: 'https://www.notion.so/my-integrations', + getKeyLabel: 'Integration 생성 β†’', + steps: ['notion.so/my-integrations λ°©λ¬Έ', 'New integration 생성', 'Internal Integration Token 볡사'], + }, + { + service: 'jira', label: 'Jira', icon: '🎯', subtitle: 'Atlassian', isAI: false, + keyLabel: 'Email:API Token', keyPlaceholder: 'user@example.com:ATATT...', + getKeyUrl: 'https://id.atlassian.com/manage-profile/security/api-tokens', + getKeyLabel: 'API 토큰 λ°œκΈ‰ β†’', + steps: ['Atlassian 계정 둜그인', 'API tokens β†’ Create API token', '"이메일:토큰" ν˜•μ‹μœΌλ‘œ μž…λ ₯\n예) user@example.com:ATATT3xFfGF0...'], + }, +]; + +type View = 'chat' | 'connectors'; + +export default function AIAgentApp() { + const [theme, setTheme] = useState<'dark' | 'light'>('dark'); + const [view, setView] = useState('chat'); + const [connectors, setConnectors] = useState([]); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [activeAI, setActiveAI] = useState('claude'); + const [configuring, setConfiguring] = useState(null); + const [keyInputs, setKeyInputs] = useState>>({}); + const [saving, setSaving] = useState(null); + const [showKey, setShowKey] = useState>>({}); + const endRef = useRef(null) as React.RefObject; + + const statusMap = Object.fromEntries( + connectors.map((c) => [c.service, c.connected]) + ) as Record; + + const connectedAIs = CONNECTORS.filter((c) => c.isAI && statusMap[c.service]); + + useEffect(() => { postMessage({ type: 'connector_load' }); }, []); + + useEffect(() => { + return onMessage((msg) => { + if (msg.type === 'connector_status') setConnectors(msg.connectors); + if (msg.type === 'ai_response' && msg.done) { + setMessages((prev) => [...prev, { role: 'assistant', content: msg.content }]); + setLoading(false); + } + if (msg.type === 'error') { + setMessages((prev) => [...prev, { role: 'assistant', content: `였λ₯˜: ${msg.message}` }]); + setLoading(false); + } + }); + }, []); + + useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); + + function openBrowser(url: string) { postMessage({ type: 'open_external', url }); } + + function saveKey(service: ConnectorService) { + const key = keyInputs[service]?.trim(); + if (!key) return; + setSaving(service); + postMessage({ type: 'connector_save', service, key }); + setKeyInputs((prev) => ({ ...prev, [service]: '' })); + setTimeout(() => { setSaving(null); setConfiguring(null); }, 1200); + } + + function disconnect(service: ConnectorService) { + postMessage({ type: 'connector_delete', service }); + if (activeAI === service) setActiveAI('claude'); + } + + function sendChat() { + const text = input.trim(); + if (!text || loading) return; + setInput(''); + if (connectedAIs.length === 0) { + setMessages((prev) => [ + ...prev, + { role: 'user', content: text }, + { role: 'assistant', content: '였λ₯Έμͺ½ Connectors νƒ­μ—μ„œ AI μ„œλΉ„μŠ€λ₯Ό λ¨Όμ € μ—°κ²°ν•΄μ£Όμ„Έμš”.' }, + ]); + return; + } + const newMessages: ChatMessage[] = [...messages, { role: 'user', content: text }]; + setMessages(newMessages); + setLoading(true); + postMessage({ type: 'ai_chat', service: activeAI, messages: newMessages, context: '' }); + } + + return ( +
+ {/* ── Header / Tab bar ── */} +
+
+ πŸ€– + AI Agent +
+ +
+ {(['chat', 'connectors'] as View[]).map((v) => ( + + ))} +
+ + {view === 'chat' && connectedAIs.length > 0 && ( +
+ {connectedAIs.map((ai) => ( + + ))} +
+ )} + + +
+ + {/* ── Chat view ── */} + {view === 'chat' && ( +
+
+ {messages.length === 0 && ( +
+
πŸ€–
+
Vespertide AI Agent
+ {connectedAIs.length === 0 ? ( +
+ AIκ°€ μ—°κ²°λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. +
+ +
+ ) : ( +
+ "이 μŠ€ν‚€λ§ˆλ₯Ό Notion에 μ •λ¦¬ν•΄μ€˜"
+ "λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 변경사항을 Slack으둜 λ³΄λ‚΄μ€˜"
+ "Post ν…Œμ΄λΈ”μ— 인덱슀λ₯Ό μΆ”κ°€ν•˜λ©΄ μ’‹μ„κΉŒ?" +
+ )} +
+ )} + + {messages.map((m, i) => { + const isUser = m.role === 'user'; + return ( +
+ {!isUser && ( + πŸ€– + )} +
+ {m.content} +
+
+ ); + })} + + {loading && ( +
+ πŸ€– + 응닡 생성 쀑... +
+ )} +
+
+ +
+