revengelibrary 0.1.5__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/MANIFEST.in +1 -1
  2. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/PKG-INFO +2 -2
  3. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/pyproject.toml +4 -3
  4. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary/__init__.py +6 -1
  5. revengelibrary-0.1.7/revengelibrary/agents.py +77 -0
  6. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary/chat.py +8 -1
  7. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary/cli.py +28 -4
  8. revengelibrary-0.1.7/revengelibrary/ide/app.js +311 -0
  9. revengelibrary-0.1.7/revengelibrary/ide/index.html +101 -0
  10. revengelibrary-0.1.7/revengelibrary/ide/styles.css +405 -0
  11. revengelibrary-0.1.7/revengelibrary/ide_server.py +480 -0
  12. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary.egg-info/PKG-INFO +2 -2
  13. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary.egg-info/SOURCES.txt +6 -1
  14. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary.egg-info/entry_points.txt +1 -0
  15. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/LICENSE +0 -0
  16. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/README.md +0 -0
  17. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary/memory_store.json +0 -0
  18. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary.egg-info/dependency_links.txt +0 -0
  19. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary.egg-info/requires.txt +0 -0
  20. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/revengelibrary.egg-info/top_level.txt +0 -0
  21. {revengelibrary-0.1.5 → revengelibrary-0.1.7}/setup.cfg +0 -0
@@ -2,7 +2,7 @@ include LICENSE
2
2
  include README.md
3
3
  include pyproject.toml
4
4
 
5
- recursive-include revengelibrary *.py *.json
5
+ recursive-include revengelibrary *.py *.json *.html *.css *.js
6
6
 
7
7
  exclude .env
8
8
  exclude .revengelibrary_memory.json
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revengelibrary
3
- Version: 0.1.5
4
- Summary: Python chat library and CLI for free LLM models via OpenRouter.
3
+ Version: 0.1.7
4
+ Summary: Не нейросеть
5
5
  Author: revengebibliotek contributors
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/example/revengelibrary
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "revengelibrary"
7
- version = "0.1.5"
8
- description = "Python chat library and CLI for free LLM models via OpenRouter."
7
+ version = "0.1.7"
8
+ description = "Не нейросеть"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { text = "MIT" }
@@ -27,6 +27,7 @@ Homepage = "https://github.com/example/revengelibrary"
27
27
 
28
28
  [project.scripts]
29
29
  revengelibrary = "revengelibrary.cli:main"
30
+ revengelibrary-ide = "revengelibrary.ide_server:main"
30
31
 
31
32
  [tool.setuptools]
32
33
  include-package-data = true
@@ -35,4 +36,4 @@ include-package-data = true
35
36
  include = ["revengelibrary*"]
36
37
 
37
38
  [tool.setuptools.package-data]
38
- revengelibrary = ["memory_store.json"]
39
+ revengelibrary = ["memory_store.json", "ide/*.html", "ide/*.css", "ide/*.js"]
@@ -1,3 +1,4 @@
1
+ from .agents import DEFAULT_AGENT, AgentProfile, get_agent, list_agents
1
2
  from .chat import (
2
3
  APIError,
3
4
  DEFAULT_MEMORY_FILE,
@@ -12,5 +13,9 @@ __all__ = [
12
13
  "DEFAULT_OPENROUTER_API_KEY",
13
14
  "DEFAULT_MODEL",
14
15
  "DEFAULT_MEMORY_FILE",
16
+ "DEFAULT_AGENT",
17
+ "AgentProfile",
18
+ "list_agents",
19
+ "get_agent",
15
20
  ]
16
- __version__ = "0.1.5"
21
+ __version__ = "0.1.7"
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ DEFAULT_AGENT = "general"
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class AgentProfile:
11
+ name: str
12
+ title: str
13
+ system_prompt: str
14
+
15
+
16
+ AGENT_PROFILES: dict[str, AgentProfile] = {
17
+ "general": AgentProfile(
18
+ name="general",
19
+ title="Универсальный ассистент",
20
+ system_prompt="You are a helpful assistant.",
21
+ ),
22
+ "frontend": AgentProfile(
23
+ name="frontend",
24
+ title="Senior Frontend + Mobile Design",
25
+ system_prompt=(
26
+ "Ты senior frontend-разработчик с сильной экспертизой в дизайне "
27
+ "мобильных приложений. Пиши ответы как практический инженер: "
28
+ "mobile-first, доступность, понятная архитектура компонентов, "
29
+ "чистый UI/UX, адаптив под iOS/Android, хорошая типографика и "
30
+ "согласованная дизайн-система."
31
+ ),
32
+ ),
33
+ "backend": AgentProfile(
34
+ name="backend",
35
+ title="Backend Engineer",
36
+ system_prompt=(
37
+ "Ты senior backend-разработчик. Предлагай надежные API-контракты, "
38
+ "чистую архитектуру, контроль ошибок, безопасность и масштабируемость."
39
+ ),
40
+ ),
41
+ "qa": AgentProfile(
42
+ name="qa",
43
+ title="QA Engineer",
44
+ system_prompt=(
45
+ "Ты senior QA-инженер. Фокус: тест-кейсы, граничные условия, "
46
+ "регрессия, воспроизводимость багов и риски релиза."
47
+ ),
48
+ ),
49
+ }
50
+
51
+
52
+ def list_agents() -> list[AgentProfile]:
53
+ return list(AGENT_PROFILES.values())
54
+
55
+
56
+ def get_agent(agent_name: str) -> AgentProfile:
57
+ name = (agent_name or DEFAULT_AGENT).strip().lower()
58
+ if name not in AGENT_PROFILES:
59
+ available = ", ".join(sorted(AGENT_PROFILES))
60
+ raise ValueError(f"Unknown agent '{agent_name}'. Available: {available}")
61
+ return AGENT_PROFILES[name]
62
+
63
+
64
+ def build_agent_memory_file(
65
+ base_memory_file: str | None,
66
+ agent_name: str,
67
+ ) -> str | None:
68
+ if not base_memory_file:
69
+ return None
70
+
71
+ if agent_name == DEFAULT_AGENT:
72
+ return base_memory_file
73
+
74
+ path = Path(base_memory_file).expanduser()
75
+ suffix = path.suffix or ".json"
76
+ stem = path.stem if path.suffix else path.name
77
+ return str(path.with_name(f"{stem}__{agent_name}{suffix}"))
@@ -176,7 +176,7 @@ class FreeNeuroChatClient:
176
176
  value = file_path if file_path is not None else self.memory_file
177
177
  if not value:
178
178
  return None
179
- return Path(value).expanduser()
179
+ return _normalize_memory_path(value)
180
180
 
181
181
  @staticmethod
182
182
  def _normalize_messages(data: Any) -> list[dict[str, str]]:
@@ -205,3 +205,10 @@ class FreeNeuroChatClient:
205
205
  return content
206
206
  except (KeyError, TypeError, IndexError):
207
207
  raise APIError(f"Unexpected API response: {data}") from None
208
+
209
+
210
+ def _normalize_memory_path(value: str) -> Path:
211
+ path = Path(value).expanduser()
212
+ if path.suffix:
213
+ return path
214
+ return path.with_suffix(".json")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import argparse
4
4
  import os
5
5
 
6
+ from .agents import DEFAULT_AGENT, build_agent_memory_file, get_agent, list_agents
6
7
  from .chat import (
7
8
  APIError,
8
9
  DEFAULT_MEMORY_FILE,
@@ -29,8 +30,18 @@ def _build_parser() -> argparse.ArgumentParser:
29
30
  )
30
31
  parser.add_argument(
31
32
  "--system",
32
- default="You are a helpful assistant.",
33
- help="System prompt.",
33
+ default=None,
34
+ help="System prompt override. If omitted, selected agent prompt is used.",
35
+ )
36
+ parser.add_argument(
37
+ "--agent",
38
+ default=os.getenv("REVENGELIBRARY_AGENT", DEFAULT_AGENT),
39
+ help="Agent role. Use --list-agents to view available roles.",
40
+ )
41
+ parser.add_argument(
42
+ "--list-agents",
43
+ action="store_true",
44
+ help="Show available agent roles and exit.",
34
45
  )
35
46
  parser.add_argument(
36
47
  "--memory-file",
@@ -47,11 +58,24 @@ def main() -> int:
47
58
  parser = _build_parser()
48
59
  args = parser.parse_args()
49
60
 
61
+ if args.list_agents:
62
+ for profile in list_agents():
63
+ print(f"{profile.name}: {profile.title}")
64
+ return 0
65
+
66
+ try:
67
+ agent = get_agent(args.agent)
68
+ except ValueError as exc:
69
+ parser.error(str(exc))
70
+
71
+ system_prompt = args.system if args.system else agent.system_prompt
72
+ memory_file = build_agent_memory_file(args.memory_file, agent.name)
73
+
50
74
  client = FreeNeuroChatClient(
51
75
  api_key=args.api_key or DEFAULT_OPENROUTER_API_KEY,
52
76
  model=args.model,
53
- system_prompt=args.system,
54
- memory_file=args.memory_file,
77
+ system_prompt=system_prompt,
78
+ memory_file=memory_file,
55
79
  )
56
80
 
57
81
  print(
@@ -0,0 +1,311 @@
1
+ const state = {
2
+ config: null,
3
+ files: [],
4
+ currentFile: null,
5
+ dirty: false,
6
+ };
7
+
8
+ const nodes = {
9
+ projectName: document.getElementById("projectName"),
10
+ filesCount: document.getElementById("filesCount"),
11
+ fileTree: document.getElementById("fileTree"),
12
+ currentFileTitle: document.getElementById("currentFileTitle"),
13
+ saveState: document.getElementById("saveState"),
14
+ editor: document.getElementById("editor"),
15
+ lineNumbers: document.getElementById("lineNumbers"),
16
+ terminal: document.getElementById("terminal"),
17
+ refreshTreeBtn: document.getElementById("refreshTreeBtn"),
18
+ newFileBtn: document.getElementById("newFileBtn"),
19
+ saveBtn: document.getElementById("saveBtn"),
20
+ formatBtn: document.getElementById("formatBtn"),
21
+ chatState: document.getElementById("chatState"),
22
+ chatLog: document.getElementById("chatLog"),
23
+ chatForm: document.getElementById("chatForm"),
24
+ chatInput: document.getElementById("chatInput"),
25
+ agentSelect: document.getElementById("agentSelect"),
26
+ modelInput: document.getElementById("modelInput"),
27
+ memoryInput: document.getElementById("memoryInput"),
28
+ };
29
+
30
+ function logLine(text, kind = "info") {
31
+ const stamp = new Date().toLocaleTimeString();
32
+ const row = `[${stamp}] [${kind.toUpperCase()}] ${text}`;
33
+ nodes.terminal.textContent += `${row}\n`;
34
+ nodes.terminal.scrollTop = nodes.terminal.scrollHeight;
35
+ }
36
+
37
+ async function fetchJSON(url, options = {}) {
38
+ const response = await fetch(url, {
39
+ headers: { "Content-Type": "application/json" },
40
+ ...options,
41
+ });
42
+ const payload = await response.json().catch(() => ({}));
43
+ if (!response.ok) {
44
+ throw new Error(payload.error || `HTTP ${response.status}`);
45
+ }
46
+ return payload;
47
+ }
48
+
49
+ function setDirty(value) {
50
+ state.dirty = value;
51
+ nodes.saveState.textContent = value ? "Есть несохраненные изменения" : "Готово";
52
+ }
53
+
54
+ function updateLineNumbers() {
55
+ const lines = nodes.editor.value.split("\n").length;
56
+ let output = "";
57
+ for (let index = 1; index <= lines; index += 1) {
58
+ output += `${index}\n`;
59
+ }
60
+ nodes.lineNumbers.textContent = output;
61
+ }
62
+
63
+ function syncScroll() {
64
+ nodes.lineNumbers.scrollTop = nodes.editor.scrollTop;
65
+ }
66
+
67
+ function appendChatMessage(kind, text) {
68
+ const row = document.createElement("div");
69
+ row.className = `msg ${kind}`;
70
+ row.textContent = text;
71
+ nodes.chatLog.appendChild(row);
72
+ nodes.chatLog.scrollTop = nodes.chatLog.scrollHeight;
73
+ }
74
+
75
+ function buildTree(paths) {
76
+ const root = { dirs: new Map(), files: [] };
77
+
78
+ for (const rawPath of paths) {
79
+ const parts = rawPath.split("/");
80
+ let cursor = root;
81
+ for (let idx = 0; idx < parts.length; idx += 1) {
82
+ const part = parts[idx];
83
+ const isFile = idx === parts.length - 1;
84
+ if (isFile) {
85
+ cursor.files.push(rawPath);
86
+ } else {
87
+ if (!cursor.dirs.has(part)) {
88
+ cursor.dirs.set(part, { dirs: new Map(), files: [] });
89
+ }
90
+ cursor = cursor.dirs.get(part);
91
+ }
92
+ }
93
+ }
94
+ return root;
95
+ }
96
+
97
+ function renderTree() {
98
+ nodes.fileTree.innerHTML = "";
99
+ const tree = buildTree(state.files);
100
+
101
+ function renderNode(node, host, prefix = "") {
102
+ const dirNames = [...node.dirs.keys()].sort((a, b) => a.localeCompare(b));
103
+ for (const dirName of dirNames) {
104
+ const row = document.createElement("div");
105
+ row.className = "tree-row";
106
+ row.textContent = `${prefix}${dirName}/`;
107
+ row.title = `${prefix}${dirName}/`;
108
+ host.appendChild(row);
109
+
110
+ const group = document.createElement("div");
111
+ group.className = "tree-group";
112
+ host.appendChild(group);
113
+ renderNode(node.dirs.get(dirName), group, `${prefix}${dirName}/`);
114
+ }
115
+
116
+ const fileNames = [...node.files].sort((a, b) => a.localeCompare(b));
117
+ for (const filePath of fileNames) {
118
+ const button = document.createElement("button");
119
+ button.type = "button";
120
+ button.className = "tree-row";
121
+ button.textContent = filePath.slice(prefix.length);
122
+ button.title = filePath;
123
+ if (state.currentFile === filePath) {
124
+ button.classList.add("active");
125
+ }
126
+ button.addEventListener("click", () => {
127
+ openFile(filePath).catch((error) => {
128
+ logLine(`Не удалось открыть ${filePath}: ${error.message}`, "error");
129
+ });
130
+ });
131
+ host.appendChild(button);
132
+ }
133
+ }
134
+
135
+ renderNode(tree, nodes.fileTree);
136
+ nodes.filesCount.textContent = `${state.files.length} files`;
137
+ }
138
+
139
+ async function loadFiles() {
140
+ const payload = await fetchJSON("/api/files");
141
+ state.files = payload.files || [];
142
+ renderTree();
143
+ }
144
+
145
+ async function openFile(path) {
146
+ if (!path) return;
147
+ const payload = await fetchJSON(`/api/file?path=${encodeURIComponent(path)}`);
148
+ state.currentFile = payload.path;
149
+ nodes.currentFileTitle.textContent = payload.path;
150
+ nodes.editor.value = payload.content || "";
151
+ updateLineNumbers();
152
+ setDirty(false);
153
+ renderTree();
154
+ logLine(`Открыт файл: ${payload.path}`);
155
+ }
156
+
157
+ async function saveFile() {
158
+ if (!state.currentFile) {
159
+ logLine("Сначала открой файл", "warn");
160
+ return;
161
+ }
162
+ await fetchJSON("/api/file", {
163
+ method: "POST",
164
+ body: JSON.stringify({
165
+ path: state.currentFile,
166
+ content: nodes.editor.value,
167
+ }),
168
+ });
169
+ setDirty(false);
170
+ logLine(`Сохранено: ${state.currentFile}`, "ok");
171
+ }
172
+
173
+ async function formatFile() {
174
+ if (!state.currentFile) {
175
+ logLine("Сначала открой файл", "warn");
176
+ return;
177
+ }
178
+ const payload = await fetchJSON("/api/format", {
179
+ method: "POST",
180
+ body: JSON.stringify({
181
+ path: state.currentFile,
182
+ content: nodes.editor.value,
183
+ }),
184
+ });
185
+ nodes.editor.value = payload.content || "";
186
+ updateLineNumbers();
187
+ setDirty(false);
188
+ logLine(`Форматирование завершено: ${state.currentFile}`, "ok");
189
+ }
190
+
191
+ async function createFile() {
192
+ const path = prompt("Новый файл (например: src/main.py)");
193
+ if (!path) return;
194
+
195
+ await fetchJSON("/api/file", {
196
+ method: "POST",
197
+ body: JSON.stringify({ path, content: "" }),
198
+ });
199
+ await loadFiles();
200
+ await openFile(path);
201
+ }
202
+
203
+ async function sendChatMessage(event) {
204
+ event.preventDefault();
205
+ const message = nodes.chatInput.value.trim();
206
+ if (!message) return;
207
+
208
+ const agent = nodes.agentSelect.value;
209
+ const model = nodes.modelInput.value.trim();
210
+ const memoryFile = nodes.memoryInput.value.trim();
211
+
212
+ appendChatMessage("user", message);
213
+ nodes.chatInput.value = "";
214
+ nodes.chatState.textContent = "Думаю...";
215
+
216
+ try {
217
+ const payload = await fetchJSON("/api/chat", {
218
+ method: "POST",
219
+ body: JSON.stringify({
220
+ message,
221
+ agent,
222
+ model,
223
+ memory_file: memoryFile || undefined,
224
+ }),
225
+ });
226
+ appendChatMessage("ai", payload.reply || "Пустой ответ");
227
+ nodes.chatState.textContent = "Ожидание";
228
+ logLine(`Ответ от агента ${payload.agent}`, "ok");
229
+ } catch (error) {
230
+ appendChatMessage("system", `Ошибка: ${error.message}`);
231
+ nodes.chatState.textContent = "Ошибка";
232
+ logLine(`Ошибка чата: ${error.message}`, "error");
233
+ }
234
+ }
235
+
236
+ function bindEvents() {
237
+ nodes.editor.addEventListener("input", () => {
238
+ setDirty(true);
239
+ updateLineNumbers();
240
+ });
241
+ nodes.editor.addEventListener("scroll", syncScroll);
242
+
243
+ nodes.saveBtn.addEventListener("click", () => {
244
+ saveFile().catch((error) => logLine(`Save error: ${error.message}`, "error"));
245
+ });
246
+ nodes.formatBtn.addEventListener("click", () => {
247
+ formatFile().catch((error) => logLine(`Format error: ${error.message}`, "error"));
248
+ });
249
+ nodes.newFileBtn.addEventListener("click", () => {
250
+ createFile().catch((error) => logLine(`Create error: ${error.message}`, "error"));
251
+ });
252
+ nodes.refreshTreeBtn.addEventListener("click", () => {
253
+ loadFiles().catch((error) => logLine(`Refresh error: ${error.message}`, "error"));
254
+ });
255
+ nodes.chatForm.addEventListener("submit", sendChatMessage);
256
+
257
+ window.addEventListener("keydown", (event) => {
258
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
259
+ event.preventDefault();
260
+ saveFile().catch((error) => logLine(`Save error: ${error.message}`, "error"));
261
+ }
262
+ if (
263
+ (event.ctrlKey || event.metaKey) &&
264
+ event.shiftKey &&
265
+ event.key.toLowerCase() === "f"
266
+ ) {
267
+ event.preventDefault();
268
+ formatFile().catch((error) =>
269
+ logLine(`Format error: ${error.message}`, "error")
270
+ );
271
+ }
272
+ });
273
+ }
274
+
275
+ async function initialize() {
276
+ bindEvents();
277
+
278
+ try {
279
+ state.config = await fetchJSON("/api/config");
280
+ nodes.projectName.textContent = state.config.root_name || "project";
281
+ nodes.modelInput.value = state.config.default_model || "";
282
+ nodes.memoryInput.value = state.config.default_memory_file || "";
283
+
284
+ for (const agent of state.config.agents || []) {
285
+ const option = document.createElement("option");
286
+ option.value = agent.name;
287
+ option.textContent = `${agent.name} - ${agent.title}`;
288
+ if (agent.name === state.config.default_agent) {
289
+ option.selected = true;
290
+ }
291
+ nodes.agentSelect.appendChild(option);
292
+ }
293
+ } catch (error) {
294
+ logLine(`Config error: ${error.message}`, "error");
295
+ }
296
+
297
+ try {
298
+ await loadFiles();
299
+ if (state.files.length > 0) {
300
+ const preferred =
301
+ state.files.find((path) => path.toLowerCase() === "readme.md") || state.files[0];
302
+ await openFile(preferred);
303
+ }
304
+ } catch (error) {
305
+ logLine(`Init error: ${error.message}`, "error");
306
+ }
307
+
308
+ appendChatMessage("system", "IDE готов. Выбери агента и отправь запрос.");
309
+ }
310
+
311
+ initialize();
@@ -0,0 +1,101 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>revengelibrary IDE</title>
7
+ <link rel="stylesheet" href="/app.css" />
8
+ </head>
9
+ <body>
10
+ <div class="ambient">
11
+ <span class="orb orb-a"></span>
12
+ <span class="orb orb-b"></span>
13
+ <span class="orb orb-c"></span>
14
+ </div>
15
+
16
+ <div class="shell">
17
+ <header class="topbar">
18
+ <div class="brand">
19
+ <div class="brand-mark">RL</div>
20
+ <div class="brand-copy">
21
+ <strong>revengelibrary IDE</strong>
22
+ <span id="projectName">project</span>
23
+ </div>
24
+ </div>
25
+ <div class="top-actions">
26
+ <button id="refreshTreeBtn" class="btn ghost">Обновить файлы</button>
27
+ <button id="newFileBtn" class="btn ghost">Новый файл</button>
28
+ <button id="saveBtn" class="btn primary">Сохранить</button>
29
+ <button id="formatBtn" class="btn accent">Форматировать</button>
30
+ </div>
31
+ </header>
32
+
33
+ <main class="workspace">
34
+ <aside class="panel files-panel">
35
+ <div class="panel-head">
36
+ <h2>Файлы</h2>
37
+ <small id="filesCount">0</small>
38
+ </div>
39
+ <div id="fileTree" class="tree"></div>
40
+ </aside>
41
+
42
+ <section class="panel editor-panel">
43
+ <div class="panel-head">
44
+ <h2 id="currentFileTitle">Файл не выбран</h2>
45
+ <small id="saveState">Готово</small>
46
+ </div>
47
+ <div class="editor-shell">
48
+ <pre id="lineNumbers" class="line-numbers">1</pre>
49
+ <textarea
50
+ id="editor"
51
+ spellcheck="false"
52
+ placeholder="Открой файл слева или создай новый"
53
+ ></textarea>
54
+ </div>
55
+ </section>
56
+
57
+ <aside class="panel assistant-panel">
58
+ <div class="panel-head">
59
+ <h2>AI-ассистент</h2>
60
+ <small id="chatState">Ожидание</small>
61
+ </div>
62
+
63
+ <div class="controls">
64
+ <label>
65
+ Агент
66
+ <select id="agentSelect"></select>
67
+ </label>
68
+ <label>
69
+ Модель
70
+ <input id="modelInput" type="text" />
71
+ </label>
72
+ <label>
73
+ Memory
74
+ <input id="memoryInput" type="text" />
75
+ </label>
76
+ </div>
77
+
78
+ <div id="chatLog" class="chat-log"></div>
79
+
80
+ <form id="chatForm" class="chat-form">
81
+ <textarea
82
+ id="chatInput"
83
+ rows="3"
84
+ placeholder="Спроси по коду, архитектуре или дизайну..."
85
+ ></textarea>
86
+ <button type="submit" class="btn primary">Отправить</button>
87
+ </form>
88
+ </aside>
89
+ </main>
90
+
91
+ <section class="panel terminal-panel">
92
+ <div class="panel-head">
93
+ <h2>Журнал</h2>
94
+ </div>
95
+ <pre id="terminal"></pre>
96
+ </section>
97
+ </div>
98
+
99
+ <script src="/app.js"></script>
100
+ </body>
101
+ </html>