scout-ai 1.0.0 → 1.0.1
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.
- checksums.yaml +4 -4
- data/.vimproject +80 -15
- data/README.md +296 -0
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/doc/Agent.md +279 -0
- data/doc/Chat.md +258 -0
- data/doc/LLM.md +446 -0
- data/doc/Model.md +513 -0
- data/doc/RAG.md +129 -0
- data/lib/scout/llm/agent/chat.rb +51 -1
- data/lib/scout/llm/agent/delegate.rb +39 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +42 -21
- data/lib/scout/llm/ask.rb +38 -6
- data/lib/scout/llm/backends/anthropic.rb +147 -0
- data/lib/scout/llm/backends/bedrock.rb +1 -1
- data/lib/scout/llm/backends/ollama.rb +23 -29
- data/lib/scout/llm/backends/openai.rb +34 -40
- data/lib/scout/llm/backends/responses.rb +158 -110
- data/lib/scout/llm/chat.rb +250 -94
- data/lib/scout/llm/embed.rb +4 -4
- data/lib/scout/llm/mcp.rb +28 -0
- data/lib/scout/llm/parse.rb +1 -0
- data/lib/scout/llm/rag.rb +9 -0
- data/lib/scout/llm/tools/call.rb +66 -0
- data/lib/scout/llm/tools/knowledge_base.rb +158 -0
- data/lib/scout/llm/tools/mcp.rb +59 -0
- data/lib/scout/llm/tools/workflow.rb +69 -0
- data/lib/scout/llm/tools.rb +58 -143
- data/lib/scout-ai.rb +1 -0
- data/scout-ai.gemspec +31 -18
- data/scout_commands/agent/ask +28 -71
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +2 -2
- data/scout_commands/llm/server +319 -0
- data/share/server/chat.html +138 -0
- data/share/server/chat.js +468 -0
- data/test/scout/llm/backends/test_anthropic.rb +134 -0
- data/test/scout/llm/backends/test_openai.rb +45 -6
- data/test/scout/llm/backends/test_responses.rb +124 -0
- data/test/scout/llm/test_agent.rb +0 -70
- data/test/scout/llm/test_ask.rb +3 -1
- data/test/scout/llm/test_chat.rb +43 -1
- data/test/scout/llm/test_mcp.rb +29 -0
- data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
- data/test/scout/llm/tools/test_mcp.rb +11 -0
- data/test/scout/llm/tools/test_workflow.rb +39 -0
- metadata +56 -17
- data/README.rdoc +0 -18
- data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
- data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
- data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
- data/python/scout_ai/atcold/plot_lib.py +0 -141
- data/python/scout_ai/atcold/spiral.py +0 -27
- data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
- data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
- data/python/scout_ai/language_model.py +0 -70
- /data/{python/scout_ai/atcold/__init__.py → test/scout/llm/tools/test_call.rb} +0 -0
@@ -0,0 +1,319 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'scout'
|
4
|
+
require 'sinatra'
|
5
|
+
require 'json'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'digest'
|
8
|
+
|
9
|
+
# Simple Sinatra server to back the offline notebook UI.
|
10
|
+
# Provides endpoints to list, load, save and run chat files stored under ./chats
|
11
|
+
|
12
|
+
set :bind, ENV.fetch('SCOUT_BIND', '127.0.0.1')
|
13
|
+
set :port, ENV.fetch('SCOUT_PORT', 4567).to_i
|
14
|
+
set :environment, :production
|
15
|
+
|
16
|
+
CHATS_DIR = File.expand_path('./chats', Dir.pwd)
|
17
|
+
FileUtils.mkdir_p(CHATS_DIR)
|
18
|
+
|
19
|
+
helpers do
|
20
|
+
def json!(obj, status = 200)
|
21
|
+
content_type :json
|
22
|
+
halt status, JSON.generate(obj)
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_json_body
|
26
|
+
body = request.body.read
|
27
|
+
return {} if body.nil? || body.strip.empty?
|
28
|
+
JSON.parse(body)
|
29
|
+
rescue JSON::ParserError
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
|
33
|
+
def sanitize_relpath(p)
|
34
|
+
return nil if p.nil? || p.to_s.strip.empty?
|
35
|
+
s = p.to_s.dup
|
36
|
+
s = s.sub(%r{\A/+}, '') # strip leading slashes
|
37
|
+
s = s.sub(%r{\Achats/}, '') # allow callers to include the prefix, normalize it away
|
38
|
+
return nil if s.include?('..')
|
39
|
+
s
|
40
|
+
end
|
41
|
+
|
42
|
+
def safe_path_for(rel)
|
43
|
+
rel_s = sanitize_relpath(rel)
|
44
|
+
raise Sinatra::NotFound unless rel_s
|
45
|
+
full = File.expand_path(File.join(CHATS_DIR, rel_s))
|
46
|
+
# ensure the resolved path is inside CHATS_DIR
|
47
|
+
unless full.start_with?(CHATS_DIR + File::SEPARATOR) || full == CHATS_DIR
|
48
|
+
raise Sinatra::NotFound
|
49
|
+
end
|
50
|
+
full
|
51
|
+
end
|
52
|
+
|
53
|
+
def etag_for(content)
|
54
|
+
Digest::SHA256.hexdigest(content.to_s)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# By default we return JSON for the API routes. Some routes will override the content_type.
|
59
|
+
before do
|
60
|
+
content_type 'application/json'
|
61
|
+
end
|
62
|
+
|
63
|
+
# Serve the web UI from the embedded share unless not available. Override the before filter's JSON content type.
|
64
|
+
get '/' do
|
65
|
+
begin
|
66
|
+
html = nil
|
67
|
+
if defined?(Scout) && Scout.respond_to?(:share) && Scout.share.respond_to?(:[]) && Scout.share['server'] && Scout.share['server']['chat.html']
|
68
|
+
html = Scout.share['server']['chat.html']
|
69
|
+
end
|
70
|
+
unless html && !html.to_s.empty?
|
71
|
+
# Fallback to the packaged file if the share entry isn't present
|
72
|
+
fallback = File.expand_path('../../../share/server/chat.html', __FILE__)
|
73
|
+
fallback = File.expand_path('./share/server/chat.html', Dir.pwd) unless File.file?(fallback)
|
74
|
+
html = File.read(fallback) if File.file?(fallback)
|
75
|
+
end
|
76
|
+
content_type 'text/html'
|
77
|
+
return html || '<html><body><h1>Chat UI not found</h1></body></html>'
|
78
|
+
rescue => e
|
79
|
+
content_type 'text/html'
|
80
|
+
return "<html><body><h1>Error rendering UI</h1><pre>#{Rack::Utils.escape_html(e.message)}</pre></body></html>"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Serve the client JS (chat.js) from the embedded share if present
|
85
|
+
get '/chat.js' do
|
86
|
+
begin
|
87
|
+
js = nil
|
88
|
+
if defined?(Scout) && Scout.respond_to?(:share) && Scout.share.respond_to?(:[]) && Scout.share['server'] && Scout.share['server']['chat.js']
|
89
|
+
js = Scout.share['server']['chat.js']
|
90
|
+
end
|
91
|
+
unless js && !js.to_s.empty?
|
92
|
+
fallback = File.expand_path('../../../share/server/chat.js', __FILE__)
|
93
|
+
fallback = File.expand_path('./share/server/chat.js', Dir.pwd) unless File.file?(fallback)
|
94
|
+
js = File.read(fallback) if File.file?(fallback)
|
95
|
+
end
|
96
|
+
content_type 'application/javascript'
|
97
|
+
return js || 'console.error("chat.js not found");'
|
98
|
+
rescue => e
|
99
|
+
content_type 'application/javascript'
|
100
|
+
return "console.error(#{e.message.inspect});"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# list files under chats directory. returns an array of relative paths (no leading 'chats/')
|
105
|
+
get '/list' do
|
106
|
+
files = []
|
107
|
+
Dir.chdir(CHATS_DIR) do
|
108
|
+
# include dotmatch in glob so we can filter hidden files/dirs uniformly
|
109
|
+
Dir.glob('**/*', File::FNM_DOTMATCH).sort.each do |f|
|
110
|
+
next if f == '.' || f == '..'
|
111
|
+
# Skip any file or directory that has a path segment starting with '.'
|
112
|
+
segments = f.split(File::SEPARATOR)
|
113
|
+
next if segments.any? { |seg| seg.start_with?('.') }
|
114
|
+
full = File.join(CHATS_DIR, f)
|
115
|
+
next if File.directory?(full)
|
116
|
+
files << f
|
117
|
+
end
|
118
|
+
end
|
119
|
+
json!(files: files)
|
120
|
+
end
|
121
|
+
|
122
|
+
# load a file's content
|
123
|
+
# GET /load?path=relative/path
|
124
|
+
get '/load' do
|
125
|
+
rel = params['path']
|
126
|
+
begin
|
127
|
+
full = safe_path_for(rel)
|
128
|
+
rescue
|
129
|
+
status 404
|
130
|
+
return JSON.generate({error: 'not_found'})
|
131
|
+
end
|
132
|
+
unless File.file?(full)
|
133
|
+
status 404
|
134
|
+
return JSON.generate({error: 'not_found'})
|
135
|
+
end
|
136
|
+
content = File.read(full)
|
137
|
+
json!(path: sanitize_relpath(rel), content: content, mtime: File.mtime(full).to_i, etag: etag_for(content))
|
138
|
+
end
|
139
|
+
|
140
|
+
# save a file
|
141
|
+
# POST /save with JSON { path: 'rel/path', content: '...' }
|
142
|
+
post '/save' do
|
143
|
+
body = parse_json_body
|
144
|
+
rel = body['path']
|
145
|
+
content = body['content'] || ''
|
146
|
+
begin
|
147
|
+
full = safe_path_for(rel)
|
148
|
+
rescue
|
149
|
+
status 400
|
150
|
+
return JSON.generate({error: 'invalid_path'})
|
151
|
+
end
|
152
|
+
dir = File.dirname(full)
|
153
|
+
FileUtils.mkdir_p(dir)
|
154
|
+
File.write(full, content)
|
155
|
+
json!(path: sanitize_relpath(rel), etag: etag_for(content), mtime: File.mtime(full).to_i)
|
156
|
+
end
|
157
|
+
|
158
|
+
# run a chat: POST /run with JSON { path: 'rel/path', content?: 'unsaved content', convo_options?: {...}, options?: {...} }
|
159
|
+
# Behavior: ensure file is saved, then run LLM chat flow and append results to the conversation file and return new content.
|
160
|
+
post '/run' do
|
161
|
+
body = parse_json_body
|
162
|
+
rel = body['path']
|
163
|
+
unless rel
|
164
|
+
status 400
|
165
|
+
return JSON.generate({error: 'missing_path'})
|
166
|
+
end
|
167
|
+
|
168
|
+
begin
|
169
|
+
full = safe_path_for(rel)
|
170
|
+
rescue
|
171
|
+
status 400
|
172
|
+
return JSON.generate({error: 'invalid_path'})
|
173
|
+
end
|
174
|
+
|
175
|
+
# If unsaved content provided, write it first
|
176
|
+
if body.key?('content')
|
177
|
+
FileUtils.mkdir_p(File.dirname(full))
|
178
|
+
File.write(full, body['content'] || '')
|
179
|
+
end
|
180
|
+
|
181
|
+
unless File.file?(full)
|
182
|
+
status 404
|
183
|
+
return JSON.generate({error: 'not_found'})
|
184
|
+
end
|
185
|
+
|
186
|
+
begin
|
187
|
+
# Load the conversation file
|
188
|
+
file_text = File.read(full)
|
189
|
+
|
190
|
+
# Attempt to call LLM to run the chat. If LLM is not available, fall back to a simulated reply.
|
191
|
+
new_messages = nil
|
192
|
+
|
193
|
+
llm_defined = defined?(LLM) && (LLM.respond_to?(:chat) || LLM.respond_to?(:ask))
|
194
|
+
|
195
|
+
if llm_defined
|
196
|
+
# If an LLM integration is present, require it to succeed. If it errors, return that error to the client
|
197
|
+
begin
|
198
|
+
conversation = nil
|
199
|
+
begin
|
200
|
+
conversation = LLM.chat(file_text)
|
201
|
+
rescue => e
|
202
|
+
# try passing the filename if the helper expects a path
|
203
|
+
begin
|
204
|
+
conversation = LLM.chat(full)
|
205
|
+
rescue => ee
|
206
|
+
# If chat failed entirely, raise to outer rescue so we return an error response
|
207
|
+
raise e
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
convo_options = body['convo_options'] || {}
|
212
|
+
ask_options = body['options'] || {}
|
213
|
+
|
214
|
+
begin
|
215
|
+
new_messages = LLM.ask(conversation, convo_options.merge(ask_options.merge(return_messages: true)))
|
216
|
+
rescue => e
|
217
|
+
# propagate the error to the client rather than falling back
|
218
|
+
raise e
|
219
|
+
end
|
220
|
+
|
221
|
+
# If the LLM ran but returned no messages, treat that as an error and provide diagnostics
|
222
|
+
if new_messages.nil? || (new_messages.respond_to?(:empty?) && new_messages.empty?)
|
223
|
+
diag = { error: 'llm_no_messages', message: 'LLM did not return any messages' }
|
224
|
+
# include some diagnostic information where safe
|
225
|
+
begin
|
226
|
+
diag[:conversation_preview] = conversation.respond_to?(:to_s) ? conversation.to_s[0,2000] : conversation.inspect[0,2000]
|
227
|
+
rescue
|
228
|
+
end
|
229
|
+
begin
|
230
|
+
diag[:convo_options] = convo_options
|
231
|
+
diag[:ask_options] = ask_options
|
232
|
+
rescue
|
233
|
+
end
|
234
|
+
json!(diag, 500)
|
235
|
+
end
|
236
|
+
rescue => e
|
237
|
+
# Return the LLM error to the client with richer diagnostics
|
238
|
+
back = (e.backtrace || [])[0,50]
|
239
|
+
err = {
|
240
|
+
error: 'llm_error',
|
241
|
+
error_class: e.class.to_s,
|
242
|
+
message: e.message,
|
243
|
+
backtrace: back
|
244
|
+
}
|
245
|
+
# include conversation and request diagnostics when available
|
246
|
+
begin
|
247
|
+
err[:conversation_preview] = conversation.respond_to?(:to_s) ? conversation.to_s[0,2000] : conversation.inspect[0,2000] if defined?(conversation) && conversation
|
248
|
+
rescue
|
249
|
+
end
|
250
|
+
begin
|
251
|
+
err[:convo_options] = convo_options if defined?(convo_options)
|
252
|
+
err[:ask_options] = ask_options if defined?(ask_options)
|
253
|
+
rescue
|
254
|
+
end
|
255
|
+
# include any partial new_messages returned (inspect up to a limit)
|
256
|
+
begin
|
257
|
+
if defined?(new_messages) && new_messages
|
258
|
+
nm = new_messages.respond_to?(:to_s) ? new_messages.to_s : new_messages.inspect
|
259
|
+
err[:new_messages_preview] = nm[0,2000]
|
260
|
+
end
|
261
|
+
rescue
|
262
|
+
end
|
263
|
+
|
264
|
+
# Do not fall back to a simulated assistant reply when an LLM error occurred; return the diagnostics instead
|
265
|
+
json!(err, 500)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
new_text = nil
|
270
|
+
if new_messages
|
271
|
+
# Try to render messages into text. Prefer LLM.print if available.
|
272
|
+
printed = nil
|
273
|
+
if defined?(LLM) && LLM.respond_to?(:print)
|
274
|
+
begin
|
275
|
+
printed = LLM.print(new_messages)
|
276
|
+
rescue
|
277
|
+
printed = nil
|
278
|
+
end
|
279
|
+
end
|
280
|
+
# If we couldn't print, try to stringify
|
281
|
+
printed ||= begin
|
282
|
+
if new_messages.respond_to?(:to_s)
|
283
|
+
new_messages.to_s
|
284
|
+
else
|
285
|
+
JSON.generate(new_messages)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
new_text = file_text + "\n" + printed
|
290
|
+
else
|
291
|
+
# If no LLM is present at all, keep the previous fallback simulated reply behavior
|
292
|
+
simulated = "assistant: (simulated reply)\n"
|
293
|
+
new_text = file_text + "\n" + simulated
|
294
|
+
end
|
295
|
+
|
296
|
+
# Save the updated conversation back to disk
|
297
|
+
File.write(full, new_text)
|
298
|
+
|
299
|
+
json!(path: sanitize_relpath(rel), content: new_text, etag: etag_for(new_text), mtime: File.mtime(full).to_i)
|
300
|
+
rescue => e
|
301
|
+
status 500
|
302
|
+
back = (e.backtrace || [])[0,50]
|
303
|
+
resp = { error: 'run_failed', error_class: e.class.to_s, message: e.message, backtrace: back }
|
304
|
+
begin
|
305
|
+
resp[:file] = full if defined?(full)
|
306
|
+
resp[:path] = sanitize_relpath(rel) if defined?(rel)
|
307
|
+
rescue
|
308
|
+
end
|
309
|
+
json!(resp, 500)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Basic health endpoint
|
314
|
+
get '/ping' do
|
315
|
+
json!(ok: true, dir: CHATS_DIR)
|
316
|
+
end
|
317
|
+
|
318
|
+
$stderr.puts "Starting scout server on #{settings.bind}:#{settings.port}, chats dir=#{CHATS_DIR}\n"
|
319
|
+
Sinatra::Application.run!
|
@@ -0,0 +1,138 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8" />
|
5
|
+
<title>Scout-AI Notebook Editor — Offline Cells (Server-backed)</title>
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7
|
+
<style>
|
8
|
+
:root{--bg:#0f1115;--panel:#151821;--muted:#9aa3b2;--border:#222838;--text:#e6e6e6;--accent:#3b82f6}
|
9
|
+
body{font-family:Inter,system-ui,Arial; margin:16px; background:var(--bg); color:var(--text)}
|
10
|
+
.wrap{max-width:880px;margin:0 auto}
|
11
|
+
header{display:flex;align-items:center;gap:12px;margin-bottom:12px}
|
12
|
+
h1{font-size:16px;margin:0}
|
13
|
+
.panel{background:var(--panel);padding:12px;border-radius:8px;border:1px solid var(--border)}
|
14
|
+
.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:12px}
|
15
|
+
.row{display:flex;gap:8px;align-items:center}
|
16
|
+
input[type="text"]{flex:1;padding:8px;border-radius:6px;border:1px solid var(--border);background:#0c0f16;color:var(--text)}
|
17
|
+
button{padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:#0c0f16;color:var(--text);cursor:pointer}
|
18
|
+
button.small{padding:5px 6px;font-size:12px}
|
19
|
+
.cells{margin-top:8px;display:flex;flex-direction:column;gap:10px}
|
20
|
+
.cell{border:1px solid transparent;border-radius:12px;padding:10px 12px;background:linear-gradient(180deg,#0b0d11, #0b0d11);position:relative;transition:box-shadow .12s, border-color .12s}
|
21
|
+
.cell:hover{box-shadow:0 4px 18px rgba(0,0,0,.6);border-color:rgba(255,255,255,0.02)}
|
22
|
+
.cell-header{display:flex;gap:8px;align-items:center;margin-bottom:8px}
|
23
|
+
.role-badge{padding:6px 8px;border-radius:999px;background:#081018;border:1px solid var(--border);color:var(--muted);font-weight:600;font-size:12px;cursor:pointer}
|
24
|
+
.role-badge.user{color:#9fdfff}
|
25
|
+
.role-badge.system{color:#c9b6ff}
|
26
|
+
.role-badge.assistant{color:#a7f3d0}
|
27
|
+
.role-badge.note{color:var(--muted)}
|
28
|
+
.cell-controls{margin-left:auto;display:flex;gap:6px;align-items:center;opacity:0;transition:opacity .12s}
|
29
|
+
.cell:hover .cell-controls{opacity:1}
|
30
|
+
.cell-actions button{background:transparent;border:1px solid transparent;padding:4px 6px;border-radius:6px;color:var(--muted);font-size:12px}
|
31
|
+
.cell-text{width:100%;min-height:56px;max-height:300px;padding:8px;border-radius:8px;border:1px solid var(--border);background:#081018;color:var(--text);font-family:ui-monospace,monospace}
|
32
|
+
.cell-input{width:100%;padding:8px;border-radius:8px;border:1px solid var(--border);background:#081018;color:var(--text)}
|
33
|
+
.muted{color:var(--muted);font-size:13px}
|
34
|
+
#log{height:72px; overflow:auto; background:#0b0d11;padding:8px;border-radius:6px;border:1px solid var(--border);color:var(--muted);font-size:13px}
|
35
|
+
.file-list{max-height:72px;overflow:auto;margin-top:8px}
|
36
|
+
.file-item{padding:4px 6px;border-radius:6px;display:flex;justify-content:space-between;gap:8px}
|
37
|
+
.file-item a{color:var(--accent);text-decoration:none}
|
38
|
+
|
39
|
+
/* loading indicator */
|
40
|
+
.loading {display:inline-flex;align-items:center;gap:8px;color:var(--accent);font-weight:600}
|
41
|
+
.spinner {width:14px;height:14px;border:2px solid rgba(255,255,255,0.08);border-top-color:var(--accent);border-radius:50%;animation:spin 1s linear infinite}
|
42
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
43
|
+
|
44
|
+
/* modal */
|
45
|
+
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:none;align-items:center;justify-content:center;z-index:999}
|
46
|
+
.modal{background:#0b0d11;color:var(--text);border:1px solid var(--border);border-radius:8px;max-width:800px;width:90%;max-height:80%;overflow:auto;padding:16px}
|
47
|
+
.modal-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
48
|
+
.modal-body{background:#081018;padding:12px;border-radius:6px}
|
49
|
+
.modal-close{margin-left:auto}
|
50
|
+
|
51
|
+
/* assistant message preview styling */
|
52
|
+
.message-preview{white-space:pre-wrap;color:var(--text)}
|
53
|
+
.assistant-bubble{background:linear-gradient(180deg,#071017,#091623);padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.02)}
|
54
|
+
.user-bubble{background:transparent;padding:8px}
|
55
|
+
|
56
|
+
/* compact toolbar */
|
57
|
+
.top-helpers{display:flex;gap:12px;align-items:flex-start}
|
58
|
+
.top-left{flex:1}
|
59
|
+
.top-right{width:280px}
|
60
|
+
|
61
|
+
@media(max-width:900px){ .wrap{margin:8px} .top-right{display:none} }
|
62
|
+
</style>
|
63
|
+
</head>
|
64
|
+
<body>
|
65
|
+
<div class="wrap">
|
66
|
+
<header>
|
67
|
+
<h1>Scout-AI Notebook Editor</h1>
|
68
|
+
<div class="small">Server-backed chat-style editor</div>
|
69
|
+
</header>
|
70
|
+
|
71
|
+
<div class="panel">
|
72
|
+
<div class="toolbar">
|
73
|
+
<div class="top-helpers" style="width:100%">
|
74
|
+
<div class="top-left">
|
75
|
+
<div style="display:flex;gap:8px;align-items:center">
|
76
|
+
<div style="width:320px;display:flex;flex-direction:column">
|
77
|
+
<label class="small">Path</label>
|
78
|
+
<input id="path" class="path-input" placeholder="chats/doc/new_chat" list="files_datalist" />
|
79
|
+
</div>
|
80
|
+
|
81
|
+
<div class="row">
|
82
|
+
<button id="newFileBtn">New</button>
|
83
|
+
<button id="loadBtn">Load</button>
|
84
|
+
<button id="saveBtn">Save</button>
|
85
|
+
<button id="runBtn">Run</button>
|
86
|
+
<button id="exportBtn">Export Text</button>
|
87
|
+
</div>
|
88
|
+
|
89
|
+
<div style="flex:1"></div>
|
90
|
+
</div>
|
91
|
+
|
92
|
+
<div style="display:flex;gap:8px;margin-top:8px;align-items:flex-start">
|
93
|
+
<div style="display:flex;gap:8px">
|
94
|
+
<button id="addUser">+ user</button>
|
95
|
+
<button id="addSystem">+ system</button>
|
96
|
+
<button id="addAssistant">+ assistant</button>
|
97
|
+
<button id="addOption">+ option</button>
|
98
|
+
<button id="clearBtn" class="small">Clear</button>
|
99
|
+
</div>
|
100
|
+
<div style="margin-left:12px" class="muted">Cells: <span id="cellCount">0</span></div>
|
101
|
+
<div style="width:12px"></div>
|
102
|
+
<div id="loadingIndicator" style="display:none" class="loading"><div class="spinner"></div><div>Running…</div></div>
|
103
|
+
</div>
|
104
|
+
</div>
|
105
|
+
|
106
|
+
<div class="top-right">
|
107
|
+
<div class="small">Workspace Files</div>
|
108
|
+
<div id="files" class="file-list"></div>
|
109
|
+
<hr style="border:0;border-top:1px solid var(--border);margin:8px 0">
|
110
|
+
<div class="small">Log</div>
|
111
|
+
<div id="log"></div>
|
112
|
+
</div>
|
113
|
+
</div>
|
114
|
+
</div>
|
115
|
+
|
116
|
+
<div id="cells" class="cells"></div>
|
117
|
+
|
118
|
+
<div style="margin-top:12px;display:flex;gap:8px;justify-content:flex-end">
|
119
|
+
<button id="runBtnBottom">Run</button>
|
120
|
+
</div>
|
121
|
+
</div>
|
122
|
+
</div>
|
123
|
+
|
124
|
+
<!-- Markdown preview modal -->
|
125
|
+
<div id="mdModal" class="modal-overlay" aria-hidden="true">
|
126
|
+
<div class="modal" role="dialog" aria-modal="true">
|
127
|
+
<div class="modal-header">
|
128
|
+
<strong id="mdTitle">Preview</strong>
|
129
|
+
<button id="mdCopy" class="small">Copy</button>
|
130
|
+
<button id="mdClose" class="modal-close">Close</button>
|
131
|
+
</div>
|
132
|
+
<div id="mdBody" class="modal-body"></div>
|
133
|
+
</div>
|
134
|
+
</div>
|
135
|
+
|
136
|
+
<script src="/chat.js" defer></script>
|
137
|
+
</body>
|
138
|
+
</html>
|