revengelibrary 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl
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.
- revengelibrary/__init__.py +1 -1
- revengelibrary/agents.py +17 -0
- revengelibrary/cli.py +2 -15
- revengelibrary/ide/app.js +311 -0
- revengelibrary/ide/index.html +101 -0
- revengelibrary/ide/styles.css +405 -0
- revengelibrary/ide_server.py +480 -0
- {revengelibrary-0.1.6.dist-info → revengelibrary-0.1.7.dist-info}/METADATA +1 -1
- revengelibrary-0.1.7.dist-info/RECORD +15 -0
- {revengelibrary-0.1.6.dist-info → revengelibrary-0.1.7.dist-info}/entry_points.txt +1 -0
- revengelibrary-0.1.6.dist-info/RECORD +0 -11
- {revengelibrary-0.1.6.dist-info → revengelibrary-0.1.7.dist-info}/WHEEL +0 -0
- {revengelibrary-0.1.6.dist-info → revengelibrary-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {revengelibrary-0.1.6.dist-info → revengelibrary-0.1.7.dist-info}/top_level.txt +0 -0
revengelibrary/__init__.py
CHANGED
revengelibrary/agents.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
DEFAULT_AGENT = "general"
|
|
6
7
|
|
|
@@ -58,3 +59,19 @@ def get_agent(agent_name: str) -> AgentProfile:
|
|
|
58
59
|
available = ", ".join(sorted(AGENT_PROFILES))
|
|
59
60
|
raise ValueError(f"Unknown agent '{agent_name}'. Available: {available}")
|
|
60
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}"))
|
revengelibrary/cli.py
CHANGED
|
@@ -2,9 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import os
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
|
|
7
|
-
from .agents import DEFAULT_AGENT, get_agent, list_agents
|
|
6
|
+
from .agents import DEFAULT_AGENT, build_agent_memory_file, get_agent, list_agents
|
|
8
7
|
from .chat import (
|
|
9
8
|
APIError,
|
|
10
9
|
DEFAULT_MEMORY_FILE,
|
|
@@ -70,7 +69,7 @@ def main() -> int:
|
|
|
70
69
|
parser.error(str(exc))
|
|
71
70
|
|
|
72
71
|
system_prompt = args.system if args.system else agent.system_prompt
|
|
73
|
-
memory_file =
|
|
72
|
+
memory_file = build_agent_memory_file(args.memory_file, agent.name)
|
|
74
73
|
|
|
75
74
|
client = FreeNeuroChatClient(
|
|
76
75
|
api_key=args.api_key or DEFAULT_OPENROUTER_API_KEY,
|
|
@@ -114,17 +113,5 @@ def main() -> int:
|
|
|
114
113
|
print(f"unexpected error: {exc}")
|
|
115
114
|
|
|
116
115
|
|
|
117
|
-
def _agent_memory_file(memory_file: str | None, agent_name: str) -> str | None:
|
|
118
|
-
if not memory_file:
|
|
119
|
-
return None
|
|
120
|
-
if agent_name == DEFAULT_AGENT:
|
|
121
|
-
return memory_file
|
|
122
|
-
|
|
123
|
-
path = Path(memory_file).expanduser()
|
|
124
|
-
suffix = path.suffix or ".json"
|
|
125
|
-
stem = path.stem if path.suffix else path.name
|
|
126
|
-
return str(path.with_name(f"{stem}__{agent_name}{suffix}"))
|
|
127
|
-
|
|
128
|
-
|
|
129
116
|
if __name__ == "__main__":
|
|
130
117
|
raise SystemExit(main())
|
|
@@ -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>
|