slidict 0.5.2 → 0.5.3
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/AGENTS.md +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +48 -2
- data/lib/slidict/cli/app.rb +144 -93
- data/lib/slidict/cli/lint.rb +26 -36
- data/lib/slidict/cli/options.rb +148 -0
- data/lib/slidict/cli/serve.rb +9 -4
- data/lib/slidict/cli/slides.rb +39 -44
- data/lib/slidict/env.rb +47 -0
- data/lib/slidict/llm/client.rb +38 -5
- data/lib/slidict/version.rb +1 -1
- data/lib/slidict.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc5350820094f49312526c30c254f437edd49374ac0202ba99067637a70dd331
|
|
4
|
+
data.tar.gz: 130980c85ff4a85a0189b95fddc825ea30eeae131e1549d83e80cee1ae5ba392
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d27d844ad6ed53f6d860fe92e8614fad35e19caa10a9e5395b4eb6640c314a0224b0c32a5de612f13cababc8e438f61c1d6103d5a5c3a8133e2021eb67be6f73
|
|
7
|
+
data.tar.gz: 241380442ea402ed968bbd7829ec8ec49446f70d7f80852d75248a829d041b09d7a9c62d9ead1341d49e6dd5beff152340c7766f822bd064c29a1e0c407fb200
|
data/AGENTS.md
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Add `slidict init` to create a `.env` file (for `SLIDICT_LLM_*`, `SLIDICT_FRAMEWORK`,
|
|
4
|
+
and `SLIDICT_METHOD`) and add it to `.gitignore`. `.env` is loaded automatically and
|
|
5
|
+
fills in unset environment variables; CLI flags still take precedence.
|
|
6
|
+
- Add `SLIDICT_FRAMEWORK` and `SLIDICT_METHOD` environment variables as defaults for
|
|
7
|
+
`--framework` and `--method`.
|
|
8
|
+
- Add `--language` to generate slide titles and bullets in a language other than English,
|
|
9
|
+
and to translate the interactive prompt questions into that language via the LLM.
|
|
10
|
+
|
|
3
11
|
## [0.5.0] - 2026-07-01
|
|
4
12
|
|
|
5
13
|
- Add data-driven presentation methods with SCQA, PREP, and Pyramid Principle templates.
|
data/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Unlike traditional slide generators, Slidict focuses on communication before sli
|
|
|
25
25
|
|
|
26
26
|
## Requirements
|
|
27
27
|
|
|
28
|
-
- Ruby 3.
|
|
28
|
+
- Ruby 3.2.0 or later
|
|
29
29
|
|
|
30
30
|
## Usage
|
|
31
31
|
|
|
@@ -94,6 +94,16 @@ Asciidoctor Reveal.js -> public/001.adoc, public/002.adoc, ...
|
|
|
94
94
|
|
|
95
95
|
## Commands
|
|
96
96
|
|
|
97
|
+
### `slidict init`
|
|
98
|
+
|
|
99
|
+
Creates a `.env` file for `SLIDICT_LLM_*`, `SLIDICT_FRAMEWORK`, and `SLIDICT_METHOD`
|
|
100
|
+
(see [Configuration](#configuration)) and adds `.env` to `.gitignore`. Does nothing to
|
|
101
|
+
an existing `.env` or an already-updated `.gitignore`.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bin/slidict init
|
|
105
|
+
```
|
|
106
|
+
|
|
97
107
|
### `slidict auth`
|
|
98
108
|
|
|
99
109
|
Authenticates the CLI with your GitHub account via the device code flow and saves a
|
|
@@ -167,19 +177,55 @@ override it with `--format`. Run `bin/slidict lint -h` for the full list of opti
|
|
|
167
177
|
## Configuration
|
|
168
178
|
|
|
169
179
|
Slidict generates slides with an LLM through any OpenAI Compatible API. Configure the
|
|
170
|
-
target endpoint with environment variables or CLI flags
|
|
180
|
+
target endpoint with environment variables, a `.env` file, or CLI flags:
|
|
171
181
|
|
|
172
182
|
| Environment variable | CLI flag | Default |
|
|
173
183
|
| ------------------------ | ---------------- | -------------- |
|
|
174
184
|
| `SLIDICT_LLM_BASE_URL` | `--llm-base-url` | _(none)_ |
|
|
175
185
|
| `SLIDICT_LLM_API_KEY` | `--llm-api-key` | _(none)_ |
|
|
176
186
|
| `SLIDICT_LLM_MODEL` | `--llm-model` | `gpt-4o-mini` |
|
|
187
|
+
| `SLIDICT_FRAMEWORK` | `--framework` | `slidev` |
|
|
188
|
+
| `SLIDICT_METHOD` | `--method` | _(none)_ |
|
|
189
|
+
|
|
190
|
+
Precedence is CLI flag > real environment variable > `.env` file > built-in default (a
|
|
191
|
+
`.env` value fills in only variables that aren't already set in the environment). Run
|
|
192
|
+
`slidict init` to create a `.env` file in the current directory (with these variables
|
|
193
|
+
commented out) and add `.env` to `.gitignore`:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
bin/slidict init
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
Created .env
|
|
201
|
+
Added .env to .gitignore
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Then edit `.env` to set the values you want, for example:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
SLIDICT_LLM_BASE_URL=http://localhost:11434/v1
|
|
208
|
+
SLIDICT_LLM_API_KEY=ollama
|
|
209
|
+
SLIDICT_LLM_MODEL=llama3
|
|
210
|
+
SLIDICT_FRAMEWORK=marp
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Since `.env` is gitignored, it's a good place for secrets like API keys that
|
|
214
|
+
shouldn't be committed. A CLI flag always overrides both `.env` and the real
|
|
215
|
+
environment for that invocation.
|
|
177
216
|
|
|
178
217
|
If no `llm-base-url` is configured, Slidict uses its built-in slide template and never
|
|
179
218
|
calls an LLM. Once a `llm-base-url` is set, Slidict always calls that endpoint; if the
|
|
180
219
|
request fails, Slidict reports the error and exits without writing a file (no fallback).
|
|
181
220
|
You can force the template even when a base URL is configured with `--no-llm`.
|
|
182
221
|
|
|
222
|
+
Use `--language LANG` (e.g. `--language Japanese`) to have the LLM write the generated
|
|
223
|
+
slide titles and bullets in a language other than English. When an `llm-base-url` is
|
|
224
|
+
also configured, Slidict asks the LLM to translate any interactive questions it still
|
|
225
|
+
needs to ask (topic, duration, audience, goal) into that language too, so you're not
|
|
226
|
+
answering Japanese-bound slides in English prompts. This only affects LLM-generated
|
|
227
|
+
slides and questions; the built-in template stays in English.
|
|
228
|
+
|
|
183
229
|
Examples:
|
|
184
230
|
|
|
185
231
|
```bash
|
data/lib/slidict/cli/app.rb
CHANGED
|
@@ -6,18 +6,58 @@ require "pathname"
|
|
|
6
6
|
module Slidict
|
|
7
7
|
module Cli
|
|
8
8
|
class App
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
include Options
|
|
10
|
+
|
|
11
|
+
# Keyed by the same symbols as the parsed CLI options (options[:topic],
|
|
12
|
+
# etc.), so a missing answer's question text can be looked up directly
|
|
13
|
+
# by `questions_for`.
|
|
14
|
+
QUESTIONS = {
|
|
15
|
+
topic: "What would you like to talk about?",
|
|
16
|
+
duration: "How long is the presentation?",
|
|
17
|
+
audience: "Who is the audience?",
|
|
18
|
+
goal: "What should the audience remember or do?"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
options input: -> { $stdin },
|
|
22
|
+
output: -> { $stdout },
|
|
23
|
+
renderer: -> { Output::Renderer.new },
|
|
24
|
+
auth_client: -> { nil },
|
|
25
|
+
credentials: -> { nil },
|
|
26
|
+
sleeper: -> { Kernel },
|
|
27
|
+
slides_command: -> { nil },
|
|
28
|
+
server: -> { nil },
|
|
29
|
+
lint_command: -> { nil }
|
|
30
|
+
|
|
31
|
+
flag "--topic", arg: "TEXT", desc: "Presentation topic"
|
|
32
|
+
flag "--duration", arg: "TEXT", desc: 'Presentation length, for example "5 minutes"'
|
|
33
|
+
flag "--audience", arg: "TEXT", desc: "Target audience"
|
|
34
|
+
flag "--goal", arg: "TEXT", desc: "Desired audience takeaway or action"
|
|
35
|
+
flag "--framework", arg: "NAME",
|
|
36
|
+
desc: -> { "#{Output::Format.names.join(", ")} (default: slidev)\n(env: SLIDICT_FRAMEWORK)" }
|
|
37
|
+
flag "--method", arg: "ID", desc: "Presentation method, for example scqa, prep, or pyramid\n" \
|
|
38
|
+
"(env: SLIDICT_METHOD)"
|
|
39
|
+
flag "--language", arg: "LANG", desc: "Generate slide titles and bullets in the given language\n" \
|
|
40
|
+
"(e.g. Japanese); only affects LLM-generated slides"
|
|
41
|
+
flag "--filename", arg: "NAME", desc: "File name under public/ (default: next sequential file)"
|
|
42
|
+
flag "--llm-base-url", arg: "URL", desc: "OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL).\n" \
|
|
43
|
+
"When omitted, the built-in slide template is used instead."
|
|
44
|
+
flag "--llm-api-key", arg: "KEY", desc: "API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)"
|
|
45
|
+
# rubocop:disable Layout/HashAlignment -- source-only line wrap, desc stays a single displayed line
|
|
46
|
+
flag "--llm-model", arg: "NAME",
|
|
47
|
+
desc: "Model name to request (env: SLIDICT_LLM_MODEL); omit to list available models"
|
|
48
|
+
# rubocop:enable Layout/HashAlignment
|
|
49
|
+
flag "--no-llm", desc: "Skip the LLM call and use the built-in slide template"
|
|
50
|
+
flag "--publish", desc: "Publish the generated slides to slidict.io as a draft\n" \
|
|
51
|
+
"(requires `slidict auth`; creates a new slide, or edits\n" \
|
|
52
|
+
"an existing one when --slide-id is given)"
|
|
53
|
+
# rubocop:disable Layout/HashAlignment -- source-only line wrap, desc stays a single displayed line
|
|
54
|
+
flag "--slide-id", arg: "ID",
|
|
55
|
+
desc: "Edit this existing draft instead of creating a new one\n(implies --publish)"
|
|
56
|
+
# rubocop:enable Layout/HashAlignment
|
|
57
|
+
flag "--slide-title", arg: "TEXT", desc: "Title for the published slide (default: --topic)"
|
|
58
|
+
flag "--visibility", arg: "VIS", desc: "public, unlisted, or group_only (default: public)"
|
|
59
|
+
flag "-o", "--output", arg: "PATH", desc: "Output file (overrides --filename and the public/ default)"
|
|
60
|
+
flag "-h", "--help", desc: "Show this help"
|
|
21
61
|
|
|
22
62
|
def run(argv = [])
|
|
23
63
|
options = parse(argv)
|
|
@@ -28,28 +68,30 @@ module Slidict
|
|
|
28
68
|
return lint(options[:args]) if options[:command] == "lint"
|
|
29
69
|
return list_methods if options[:command] == "list-methods"
|
|
30
70
|
return show_method(options[:args]) if options[:command] == "show-method"
|
|
71
|
+
return init if options[:command] == "init"
|
|
31
72
|
|
|
32
73
|
config = build_config(options)
|
|
33
74
|
return print_available_models(config) if config.llm_enabled? && config.model.nil?
|
|
34
75
|
|
|
35
76
|
client = llm_client_for(config)
|
|
36
|
-
return
|
|
77
|
+
return FAILURE if client && !verify_connection(client)
|
|
37
78
|
|
|
79
|
+
questions = questions_for(client, options)
|
|
38
80
|
deck = Deck.new(
|
|
39
|
-
topic: ask(
|
|
40
|
-
duration: ask(
|
|
41
|
-
audience: ask(
|
|
42
|
-
goal: ask(
|
|
81
|
+
topic: ask(questions[:topic], options[:topic]),
|
|
82
|
+
duration: ask(questions[:duration], options[:duration]),
|
|
83
|
+
audience: ask(questions[:audience], options[:audience]),
|
|
84
|
+
goal: ask(questions[:goal], options[:goal]),
|
|
43
85
|
framework: options[:framework],
|
|
44
86
|
presentation_method: options[:presentation_method]
|
|
45
87
|
)
|
|
46
88
|
|
|
47
89
|
if client
|
|
48
90
|
begin
|
|
49
|
-
slides = client.generate_slides(deck)
|
|
91
|
+
slides = client.generate_slides(deck, language: options[:language])
|
|
50
92
|
rescue Llm::Client::Error => e
|
|
51
93
|
@output.puts "Error: LLM request failed (#{e.message})"
|
|
52
|
-
return
|
|
94
|
+
return FAILURE
|
|
53
95
|
end
|
|
54
96
|
deck = Deck.new(
|
|
55
97
|
topic: deck.topic, duration: deck.duration, audience: deck.audience, goal: deck.goal,
|
|
@@ -65,18 +107,18 @@ module Slidict
|
|
|
65
107
|
|
|
66
108
|
return publish_to_slidict(deck, content, options) if options[:publish] || options[:slide_id]
|
|
67
109
|
|
|
68
|
-
|
|
110
|
+
SUCCESS
|
|
69
111
|
rescue ArgumentError => e
|
|
70
112
|
@output.puts "Error: #{e.message}"
|
|
71
113
|
@output.puts
|
|
72
114
|
print_help
|
|
73
|
-
|
|
115
|
+
FAILURE
|
|
74
116
|
end
|
|
75
117
|
|
|
76
118
|
private
|
|
77
119
|
|
|
78
120
|
def parse(argv)
|
|
79
|
-
options = { framework: "slidev" }
|
|
121
|
+
options = { framework: ENV["SLIDICT_FRAMEWORK"] || "slidev", method: ENV["SLIDICT_METHOD"] }
|
|
80
122
|
args = argv.dup
|
|
81
123
|
|
|
82
124
|
args.shift if args.first == "new"
|
|
@@ -125,47 +167,16 @@ module Slidict
|
|
|
125
167
|
return options
|
|
126
168
|
end
|
|
127
169
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
when "--filename"
|
|
135
|
-
options[:filename] = fetch_value!(args, arg)
|
|
136
|
-
when "--topic"
|
|
137
|
-
options[:topic] = fetch_value!(args, arg)
|
|
138
|
-
when "--duration"
|
|
139
|
-
options[:duration] = fetch_value!(args, arg)
|
|
140
|
-
when "--audience"
|
|
141
|
-
options[:audience] = fetch_value!(args, arg)
|
|
142
|
-
when "--goal"
|
|
143
|
-
options[:goal] = fetch_value!(args, arg)
|
|
144
|
-
when "--framework"
|
|
145
|
-
options[:framework] = fetch_value!(args, arg)
|
|
146
|
-
when "--method"
|
|
147
|
-
options[:method] = fetch_value!(args, arg)
|
|
148
|
-
when "--llm-base-url"
|
|
149
|
-
options[:llm_base_url] = fetch_value!(args, arg)
|
|
150
|
-
when "--llm-api-key"
|
|
151
|
-
options[:llm_api_key] = fetch_value!(args, arg)
|
|
152
|
-
when "--llm-model"
|
|
153
|
-
options[:llm_model] = fetch_value!(args, arg)
|
|
154
|
-
when "--no-llm"
|
|
155
|
-
options[:no_llm] = true
|
|
156
|
-
when "--publish"
|
|
157
|
-
options[:publish] = true
|
|
158
|
-
when "--slide-id"
|
|
159
|
-
options[:slide_id] = fetch_value!(args, arg)
|
|
160
|
-
when "--slide-title"
|
|
161
|
-
options[:slide_title] = fetch_value!(args, arg)
|
|
162
|
-
when "--visibility"
|
|
163
|
-
options[:visibility] = fetch_value!(args, arg)
|
|
164
|
-
else
|
|
165
|
-
raise ArgumentError, "unknown option #{arg}"
|
|
166
|
-
end
|
|
170
|
+
if args.first == "init"
|
|
171
|
+
args.shift
|
|
172
|
+
raise ArgumentError, "init does not accept options" unless args.empty?
|
|
173
|
+
|
|
174
|
+
options[:command] = "init"
|
|
175
|
+
return options
|
|
167
176
|
end
|
|
168
177
|
|
|
178
|
+
parse_flags!(args, options)
|
|
179
|
+
|
|
169
180
|
options[:output] ||= output_path_for(options[:framework], options[:filename])
|
|
170
181
|
options[:presentation_method] = method_for(options[:method])
|
|
171
182
|
options
|
|
@@ -189,10 +200,10 @@ module Slidict
|
|
|
189
200
|
@output.puts "Available models (specify one with --llm-model NAME or SLIDICT_LLM_MODEL=NAME):"
|
|
190
201
|
models.each { |m| @output.puts " #{m}" }
|
|
191
202
|
end
|
|
192
|
-
|
|
203
|
+
SUCCESS
|
|
193
204
|
rescue Llm::Client::Error => e
|
|
194
205
|
@output.puts "Error: LLM request failed (#{e.message})"
|
|
195
|
-
|
|
206
|
+
FAILURE
|
|
196
207
|
end
|
|
197
208
|
|
|
198
209
|
def llm_client_for(config)
|
|
@@ -228,7 +239,7 @@ module Slidict
|
|
|
228
239
|
provider: token.fetch("provider", "github")
|
|
229
240
|
)
|
|
230
241
|
@output.puts "4. Saved CLI access token to #{path}"
|
|
231
|
-
return
|
|
242
|
+
return SUCCESS
|
|
232
243
|
rescue External::SlidictIo::Auth::Pending
|
|
233
244
|
return login_expired if Time.now >= deadline
|
|
234
245
|
|
|
@@ -236,7 +247,7 @@ module Slidict
|
|
|
236
247
|
end
|
|
237
248
|
rescue External::SlidictIo::Auth::Error, KeyError => e
|
|
238
249
|
@output.puts "Error: GitHub auth failed (#{e.message})"
|
|
239
|
-
|
|
250
|
+
FAILURE
|
|
240
251
|
end
|
|
241
252
|
|
|
242
253
|
def slides(args)
|
|
@@ -258,7 +269,7 @@ module Slidict
|
|
|
258
269
|
id: method.id, name: method.name, category: method.category
|
|
259
270
|
)
|
|
260
271
|
end
|
|
261
|
-
|
|
272
|
+
SUCCESS
|
|
262
273
|
end
|
|
263
274
|
|
|
264
275
|
def show_method(args)
|
|
@@ -275,13 +286,53 @@ module Slidict
|
|
|
275
286
|
method.slides.each_with_index { |slide, i| @output.puts " #{i + 1}. #{slide.title}: #{slide.role}" }
|
|
276
287
|
@output.puts "Review checklist:"
|
|
277
288
|
method.review_checklist.each { |item| @output.puts " - #{item}" }
|
|
278
|
-
|
|
289
|
+
SUCCESS
|
|
279
290
|
end
|
|
280
291
|
|
|
281
292
|
def method_for(id)
|
|
282
293
|
id ? method_registry.fetch(id) : nil
|
|
283
294
|
end
|
|
284
295
|
|
|
296
|
+
ENV_TEMPLATE = <<~ENV
|
|
297
|
+
# Slidict configuration. CLI flags always take precedence over these
|
|
298
|
+
# values; unset ones fall back to the built-in defaults. This file is
|
|
299
|
+
# ignored by git (see .gitignore) so it's safe to put secrets here.
|
|
300
|
+
|
|
301
|
+
# OpenAI Compatible API endpoint (required to enable LLM-generated slides).
|
|
302
|
+
# SLIDICT_LLM_BASE_URL=https://api.openai.com/v1
|
|
303
|
+
# SLIDICT_LLM_API_KEY=sk-...
|
|
304
|
+
# SLIDICT_LLM_MODEL=gpt-4o-mini
|
|
305
|
+
|
|
306
|
+
# Default --framework when it is not given on the command line.
|
|
307
|
+
# SLIDICT_FRAMEWORK=slidev
|
|
308
|
+
|
|
309
|
+
# Default --method when it is not given on the command line.
|
|
310
|
+
# SLIDICT_METHOD=scqa
|
|
311
|
+
ENV
|
|
312
|
+
|
|
313
|
+
def init
|
|
314
|
+
env_created = write_env_file
|
|
315
|
+
@output.puts(env_created ? "Created .env" : ".env already exists, leaving it unchanged")
|
|
316
|
+
|
|
317
|
+
gitignore_updated = ensure_env_gitignored
|
|
318
|
+
@output.puts "Added .env to .gitignore" if gitignore_updated
|
|
319
|
+
SUCCESS
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def write_env_file
|
|
323
|
+
return false if File.exist?(".env")
|
|
324
|
+
|
|
325
|
+
File.write(".env", ENV_TEMPLATE)
|
|
326
|
+
true
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def ensure_env_gitignored
|
|
330
|
+
return false if File.exist?(".gitignore") && File.readlines(".gitignore", chomp: true).include?(".env")
|
|
331
|
+
|
|
332
|
+
File.open(".gitignore", "a") { |f| f.puts(".env") }
|
|
333
|
+
true
|
|
334
|
+
end
|
|
335
|
+
|
|
285
336
|
def method_registry
|
|
286
337
|
@method_registry ||= PresentationMethodRegistry.new
|
|
287
338
|
end
|
|
@@ -314,7 +365,7 @@ module Slidict
|
|
|
314
365
|
|
|
315
366
|
def login_expired
|
|
316
367
|
@output.puts "Error: GitHub auth timed out. Run `slidict auth` and try again."
|
|
317
|
-
|
|
368
|
+
FAILURE
|
|
318
369
|
end
|
|
319
370
|
|
|
320
371
|
def fetch_value!(args, option)
|
|
@@ -332,9 +383,29 @@ module Slidict
|
|
|
332
383
|
@input.gets&.chomp.to_s
|
|
333
384
|
end
|
|
334
385
|
|
|
335
|
-
|
|
336
|
-
|
|
386
|
+
# Translates only the questions that will actually be asked (those
|
|
387
|
+
# whose option wasn't already given on the command line). Falls back to
|
|
388
|
+
# the English questions if there's no client, no --language, nothing
|
|
389
|
+
# left to ask, or a translation call fails -- this is a nicety, not
|
|
390
|
+
# something worth aborting slide generation over.
|
|
391
|
+
def questions_for(client, options)
|
|
392
|
+
return QUESTIONS unless client && options[:language]
|
|
393
|
+
|
|
394
|
+
missing = QUESTIONS.select { |key, _| options[key].to_s.strip.empty? }
|
|
395
|
+
return QUESTIONS if missing.empty?
|
|
396
|
+
|
|
397
|
+
translated = missing.transform_values { |text| client.translate_text(text, options[:language]) }
|
|
398
|
+
QUESTIONS.merge(translated)
|
|
399
|
+
rescue Llm::Client::Error => e
|
|
400
|
+
@output.puts "Warning: could not translate questions into #{options[:language]} " \
|
|
401
|
+
"(#{e.message}); asking in English."
|
|
402
|
+
QUESTIONS
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
usage do
|
|
406
|
+
<<~USAGE
|
|
337
407
|
Usage: slidict [new] [options]
|
|
408
|
+
Usage: slidict init
|
|
338
409
|
Usage: slidict auth
|
|
339
410
|
Usage: slidict slides <list|show|create|edit> [options]
|
|
340
411
|
Usage: slidict serve [sinatra options]
|
|
@@ -345,6 +416,7 @@ module Slidict
|
|
|
345
416
|
Generate presentation source files from a short conversation.
|
|
346
417
|
|
|
347
418
|
Commands:
|
|
419
|
+
init Create a .env file for SLIDICT_LLM_* etc. and add it to .gitignore
|
|
348
420
|
auth Authenticate the CLI with GitHub and save a CLI access token
|
|
349
421
|
slides Manage your slides on slidict.io (run `slidict slides -h` for details)
|
|
350
422
|
serve Serve slide files from ./public with Sinatra
|
|
@@ -354,29 +426,8 @@ module Slidict
|
|
|
354
426
|
show-method Show details for one presentation method
|
|
355
427
|
|
|
356
428
|
Options:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
--audience TEXT Target audience
|
|
360
|
-
--goal TEXT Desired audience takeaway or action
|
|
361
|
-
--framework NAME #{Output::Format.names.join(", ")} (default: slidev)
|
|
362
|
-
--method ID Presentation method, for example scqa, prep, or pyramid
|
|
363
|
-
--filename NAME File name under public/ (default: next sequential file)
|
|
364
|
-
--llm-base-url URL OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL).
|
|
365
|
-
When omitted, the built-in slide template is used instead.
|
|
366
|
-
--llm-api-key KEY API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)
|
|
367
|
-
--llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL); omit to list available models
|
|
368
|
-
--no-llm Skip the LLM call and use the built-in slide template
|
|
369
|
-
--publish Publish the generated slides to slidict.io as a draft
|
|
370
|
-
(requires `slidict auth`; creates a new slide, or edits
|
|
371
|
-
an existing one when --slide-id is given)
|
|
372
|
-
--slide-id ID Edit this existing draft instead of creating a new one
|
|
373
|
-
(implies --publish)
|
|
374
|
-
--slide-title TEXT Title for the published slide (default: --topic)
|
|
375
|
-
--visibility VIS public, unlisted, or group_only (default: public)
|
|
376
|
-
-o, --output PATH Output file (overrides --filename and the public/ default)
|
|
377
|
-
-h, --help Show this help
|
|
378
|
-
HELP
|
|
379
|
-
0
|
|
429
|
+
#{flags_help}
|
|
430
|
+
USAGE
|
|
380
431
|
end
|
|
381
432
|
|
|
382
433
|
def output_path_for(framework, filename)
|
data/lib/slidict/cli/lint.rb
CHANGED
|
@@ -8,11 +8,21 @@ module Slidict
|
|
|
8
8
|
class Lint
|
|
9
9
|
ASCIIDOC_EXTENSIONS = %w[.adoc .asciidoc].freeze
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
include Options
|
|
12
|
+
|
|
13
|
+
options output: Options::MISSING,
|
|
14
|
+
linter_factory: -> { method(:default_linter) },
|
|
15
|
+
renderer: -> { Slidict::Lint::Renderer.new }
|
|
16
|
+
|
|
17
|
+
flag "--format", arg: "FORMAT", desc: "markdown or asciidoc (default: auto-detected from extension)"
|
|
18
|
+
flag "--llm-base-url", arg: "URL", desc: "OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL)"
|
|
19
|
+
flag "--llm-api-key", arg: "KEY", desc: "API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)"
|
|
20
|
+
# rubocop:disable Layout/HashAlignment -- source-only line wrap, desc stays a single displayed line
|
|
21
|
+
flag "--llm-model", arg: "NAME",
|
|
22
|
+
desc: "Model name to request (env: SLIDICT_LLM_MODEL); omit to list available models"
|
|
23
|
+
# rubocop:enable Layout/HashAlignment
|
|
24
|
+
flag "--translate", arg: "LANG", desc: "Translate findings into the given language (e.g. Japanese)"
|
|
25
|
+
flag "-h", "--help", desc: "Show this help"
|
|
16
26
|
|
|
17
27
|
def run(argv)
|
|
18
28
|
options = parse(argv)
|
|
@@ -44,18 +54,18 @@ module Slidict
|
|
|
44
54
|
print_error(error)
|
|
45
55
|
@output.puts
|
|
46
56
|
print_help
|
|
47
|
-
|
|
57
|
+
FAILURE
|
|
48
58
|
end
|
|
49
59
|
|
|
50
60
|
def print_error(error)
|
|
51
61
|
@output.puts "Error: #{error.message}"
|
|
52
|
-
|
|
62
|
+
FAILURE
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
def parse(argv)
|
|
56
66
|
args = argv.dup
|
|
57
67
|
options = { path: extract_path!(args) }
|
|
58
|
-
|
|
68
|
+
parse_flags!(args, options)
|
|
59
69
|
options
|
|
60
70
|
end
|
|
61
71
|
|
|
@@ -63,20 +73,6 @@ module Slidict
|
|
|
63
73
|
args.shift unless args.first.to_s.start_with?("-")
|
|
64
74
|
end
|
|
65
75
|
|
|
66
|
-
def parse_options!(args, options)
|
|
67
|
-
until args.empty?
|
|
68
|
-
case (arg = args.shift)
|
|
69
|
-
when "-h", "--help" then options[:help] = true
|
|
70
|
-
when "--format" then options[:format] = fetch_value!(args, arg)
|
|
71
|
-
when "--llm-base-url" then options[:llm_base_url] = fetch_value!(args, arg)
|
|
72
|
-
when "--llm-api-key" then options[:llm_api_key] = fetch_value!(args, arg)
|
|
73
|
-
when "--llm-model" then options[:llm_model] = fetch_value!(args, arg)
|
|
74
|
-
when "--translate" then options[:translate] = fetch_value!(args, arg)
|
|
75
|
-
else raise ArgumentError, "unknown option #{arg}"
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
76
|
def fetch_value!(args, option)
|
|
81
77
|
value = args.shift
|
|
82
78
|
raise ArgumentError, "#{option} requires a value" if value.nil? || value.start_with?("-")
|
|
@@ -105,12 +101,12 @@ module Slidict
|
|
|
105
101
|
|
|
106
102
|
def print_findings(findings)
|
|
107
103
|
@output.puts(findings.empty? ? "No issues found." : @renderer.render(findings))
|
|
108
|
-
|
|
104
|
+
SUCCESS
|
|
109
105
|
end
|
|
110
106
|
|
|
111
107
|
def file_not_found(path)
|
|
112
108
|
@output.puts "Error: file not found: #{path}"
|
|
113
|
-
|
|
109
|
+
FAILURE
|
|
114
110
|
end
|
|
115
111
|
|
|
116
112
|
def print_available_models(config)
|
|
@@ -122,28 +118,22 @@ module Slidict
|
|
|
122
118
|
@output.puts "Available models (specify one with --llm-model NAME or SLIDICT_LLM_MODEL=NAME):"
|
|
123
119
|
models.each { |m| @output.puts " #{m}" }
|
|
124
120
|
end
|
|
125
|
-
|
|
121
|
+
SUCCESS
|
|
126
122
|
rescue Llm::Client::Error => e
|
|
127
123
|
print_error(e)
|
|
128
124
|
end
|
|
129
125
|
|
|
130
126
|
def llm_required
|
|
131
127
|
@output.puts "Error: lint requires an LLM endpoint (--llm-base-url or SLIDICT_LLM_BASE_URL)"
|
|
132
|
-
|
|
128
|
+
FAILURE
|
|
133
129
|
end
|
|
134
130
|
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
usage do
|
|
132
|
+
<<~USAGE
|
|
137
133
|
Usage: slidict lint <file> [options]
|
|
138
134
|
Diagnoses whether a slide deck's structure will land with its audience.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
--llm-api-key KEY API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)
|
|
142
|
-
--llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL); omit to list available models
|
|
143
|
-
--translate LANG Translate findings into the given language (e.g. Japanese)
|
|
144
|
-
-h, --help Show this help
|
|
145
|
-
HELP
|
|
146
|
-
0
|
|
135
|
+
#{flags_help}
|
|
136
|
+
USAGE
|
|
147
137
|
end
|
|
148
138
|
end
|
|
149
139
|
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slidict
|
|
4
|
+
module Cli
|
|
5
|
+
# Concern for CLI classes with several constructor dependencies. Declaring
|
|
6
|
+
# `options name: -> { default }, ...` once generates both the attr_reader
|
|
7
|
+
# and the `initialize` assignment, instead of repeating each dependency in
|
|
8
|
+
# the method signature and again as `@name = name` in the body.
|
|
9
|
+
#
|
|
10
|
+
# Defaults are instance_exec'd lazily (only when the keyword is omitted),
|
|
11
|
+
# matching plain keyword-argument defaults, and may reference other
|
|
12
|
+
# options or instance methods (e.g. `-> { method(:default_linter) }`).
|
|
13
|
+
# Classes that need to post-process a value (e.g. expanding a path) can
|
|
14
|
+
# still override `initialize`, call `super`, and adjust the ivar.
|
|
15
|
+
#
|
|
16
|
+
# Also provides a `flag`/`parse_flags!`/`flags_help` DSL: declaring a
|
|
17
|
+
# `-x`/`--xxx` switch once (with its arg placeholder and description)
|
|
18
|
+
# drives both argv parsing and the `--help` listing, so the two can't
|
|
19
|
+
# drift apart. `usage` then only needs to hold the surrounding usage text
|
|
20
|
+
# (Usage:/Commands: lines), interpolating `flags_help` for the option list.
|
|
21
|
+
module Options
|
|
22
|
+
MISSING = Object.new.freeze
|
|
23
|
+
|
|
24
|
+
# Process exit codes (see bin/slidict, which does `exit App.new.run(ARGV)`),
|
|
25
|
+
# named so command methods don't scatter bare 0/1 literals as return values.
|
|
26
|
+
SUCCESS = 0
|
|
27
|
+
FAILURE = 1
|
|
28
|
+
|
|
29
|
+
def self.included(base)
|
|
30
|
+
base.extend(ClassMethods)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# One `-x`/`--xxx` declaration: how to parse it (arg placeholder,
|
|
34
|
+
# coercion) and how to describe it in `--help` output.
|
|
35
|
+
Flag = Struct.new(:switches, :key, :arg, :coerce, :desc, keyword_init: true) do
|
|
36
|
+
def boolean?
|
|
37
|
+
arg.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def short
|
|
41
|
+
switches.find { |s| !s.start_with?("--") }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def long
|
|
45
|
+
switches.find { |s| s.start_with?("--") }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Left column of the help listing, e.g. "-o, --output PATH" or, for a
|
|
49
|
+
# long-only flag, " --topic TEXT" -- the leading 4 spaces keep the
|
|
50
|
+
# "--name" text column-aligned either way.
|
|
51
|
+
def name_column
|
|
52
|
+
column = "#{short ? "#{short}, " : " "}#{long}"
|
|
53
|
+
arg ? "#{column} #{arg}" : column
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Class-level DSL (`options name: -> { default }, ...`) mixed into any
|
|
58
|
+
# class that includes Options.
|
|
59
|
+
module ClassMethods
|
|
60
|
+
# Defines the assignment in an anonymous module (rather than directly
|
|
61
|
+
# on the including class) so a class that needs to post-process a
|
|
62
|
+
# value can still define its own `initialize`, call `super`, and have
|
|
63
|
+
# it reach this one instead of silently overwriting it.
|
|
64
|
+
def options(**defaults)
|
|
65
|
+
attr_reader(*defaults.keys)
|
|
66
|
+
|
|
67
|
+
include(Module.new do
|
|
68
|
+
define_method(:initialize) do |**overrides|
|
|
69
|
+
defaults.each do |name, default|
|
|
70
|
+
instance_variable_set(:"@#{name}", resolve_option(name, default, overrides))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Declares a flag. `group` lets a class with several argv shapes
|
|
77
|
+
# (e.g. Slides' `list` vs. `create`/`edit`) keep separate flag sets;
|
|
78
|
+
# `parse_flags!`/`flags_help` default to :default. `desc` may be a
|
|
79
|
+
# Proc (instance_exec'd lazily, like an `options` default) for text
|
|
80
|
+
# that depends on another file having loaded by call time rather
|
|
81
|
+
# than by require time (e.g. `Output::Format.names`).
|
|
82
|
+
def flag(*switches, desc:, arg: nil, coerce: nil, group: :default)
|
|
83
|
+
key = switches.last.sub(/\A-+/, "").tr("-", "_").to_sym
|
|
84
|
+
flag_groups[group] << Flag.new(switches: switches, key: key, arg: arg, coerce: coerce, desc: desc)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def flag_groups
|
|
88
|
+
@flag_groups ||= Hash.new { |h, k| h[k] = [] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def flags(group = :default)
|
|
92
|
+
flag_groups[group]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Declares the `-h`/`--help` usage text, generating a private
|
|
96
|
+
# `print_help` that writes it to `@output` and returns SUCCESS,
|
|
97
|
+
# instead of every command spelling that out. Takes a block for the
|
|
98
|
+
# same lazy-evaluation reason as `flag`'s `desc:`.
|
|
99
|
+
def usage(&text)
|
|
100
|
+
private(define_method(:print_help) do
|
|
101
|
+
@output.puts instance_exec(&text)
|
|
102
|
+
SUCCESS
|
|
103
|
+
end)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def resolve_option(name, default, overrides)
|
|
110
|
+
return overrides[name] if overrides.key?(name)
|
|
111
|
+
raise ArgumentError, "missing keyword: :#{name}" if default.equal?(MISSING)
|
|
112
|
+
|
|
113
|
+
instance_exec(&default)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Consumes `args` against `self.class.flags(group)`: booleans are set to
|
|
117
|
+
# true, valued flags read their value via the including class's own
|
|
118
|
+
# `fetch_value!` (whose rules on e.g. a leading "-" differ between
|
|
119
|
+
# classes), then any `coerce` is applied.
|
|
120
|
+
def parse_flags!(args, options, group = :default)
|
|
121
|
+
until args.empty?
|
|
122
|
+
arg = args.shift
|
|
123
|
+
flag = self.class.flags(group).find { |f| f.switches.include?(arg) }
|
|
124
|
+
raise ArgumentError, "unknown option #{arg}" unless flag
|
|
125
|
+
|
|
126
|
+
value = flag.boolean? || fetch_value!(args, arg)
|
|
127
|
+
value = instance_exec(value, &flag.coerce) if flag.coerce
|
|
128
|
+
options[flag.key] = value
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Renders `self.class.flags(group)` as an aligned --help listing.
|
|
133
|
+
def flags_help(group = :default)
|
|
134
|
+
defs = self.class.flags(group)
|
|
135
|
+
width = defs.map { |f| f.name_column.length }.max
|
|
136
|
+
defs.map { |f| flag_doc_lines(f, width) }.join("\n")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def flag_doc_lines(flag, width)
|
|
140
|
+
desc = flag.desc.respond_to?(:call) ? instance_exec(&flag.desc) : flag.desc
|
|
141
|
+
first, *rest = desc.lines(chomp: true)
|
|
142
|
+
lines = ["#{flag.name_column.ljust(width)} #{first}"]
|
|
143
|
+
rest.each { |line| lines << "#{" " * (width + 2)}#{line}" }
|
|
144
|
+
lines.join("\n")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/slidict/cli/serve.rb
CHANGED
|
@@ -9,9 +9,14 @@ module Slidict
|
|
|
9
9
|
class Serve
|
|
10
10
|
DEFAULT_PUBLIC_DIR = "public"
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
include Options
|
|
13
|
+
|
|
14
|
+
options public_dir: -> { DEFAULT_PUBLIC_DIR },
|
|
15
|
+
output: -> { $stdout }
|
|
16
|
+
|
|
17
|
+
def initialize(**)
|
|
18
|
+
super
|
|
19
|
+
@public_dir = File.expand_path(@public_dir)
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def run(args = [])
|
|
@@ -20,7 +25,7 @@ module Slidict
|
|
|
20
25
|
ARGV.replace(args)
|
|
21
26
|
@output.puts "Serving slides from #{@public_dir}"
|
|
22
27
|
app.run!
|
|
23
|
-
|
|
28
|
+
SUCCESS
|
|
24
29
|
ensure
|
|
25
30
|
ARGV.replace(original_argv) if original_argv
|
|
26
31
|
end
|
data/lib/slidict/cli/slides.rb
CHANGED
|
@@ -4,12 +4,29 @@ module Slidict
|
|
|
4
4
|
module Cli
|
|
5
5
|
# Implements the `slidict slides <list|show|create|edit>` subcommands.
|
|
6
6
|
class Slides
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
include Options
|
|
8
|
+
|
|
9
|
+
options output: Options::MISSING,
|
|
10
|
+
credentials: -> { External::SlidictIo::Credentials.new },
|
|
11
|
+
client: -> { nil },
|
|
12
|
+
reauthenticate: -> { nil }
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Layout/HashAlignment -- source-only line wraps below, kept short rather than column-aligned
|
|
15
|
+
flag "--page", arg: "N",
|
|
16
|
+
coerce: ->(v) { Integer(v) }, desc: "Page number (20 slides per page)", group: :list
|
|
17
|
+
# rubocop:enable Layout/HashAlignment
|
|
18
|
+
flag "-h", "--help", desc: "Show this help", group: :list
|
|
19
|
+
|
|
20
|
+
flag "--title", arg: "TEXT", desc: "Slide title", group: :body_options
|
|
21
|
+
flag "--body", arg: "TEXT", desc: "Slide body text", group: :body_options
|
|
22
|
+
# rubocop:disable Layout/HashAlignment -- source-only line wraps below, kept short rather than column-aligned
|
|
23
|
+
flag "--file", arg: "PATH", desc: "Read the slide body from a file (instead of --body)",
|
|
24
|
+
group: :body_options
|
|
25
|
+
flag "--body-format", arg: "FORMAT", desc: "asciidoc or markdown (default: auto-detected from body)",
|
|
26
|
+
group: :body_options
|
|
27
|
+
# rubocop:enable Layout/HashAlignment
|
|
28
|
+
flag "--visibility", arg: "VIS", desc: "public, unlisted, or group_only (default: public)", group: :body_options
|
|
29
|
+
flag "-h", "--help", desc: "Show this help", group: :body_options
|
|
13
30
|
|
|
14
31
|
def run(argv)
|
|
15
32
|
options = parse(argv)
|
|
@@ -20,7 +37,7 @@ module Slidict
|
|
|
20
37
|
@output.puts "Error: #{e.message}"
|
|
21
38
|
@output.puts
|
|
22
39
|
print_help
|
|
23
|
-
|
|
40
|
+
FAILURE
|
|
24
41
|
end
|
|
25
42
|
|
|
26
43
|
# Creates or edits a draft directly, bypassing argv parsing (and its
|
|
@@ -49,13 +66,7 @@ module Slidict
|
|
|
49
66
|
|
|
50
67
|
def parse_list(args)
|
|
51
68
|
options = { subcommand: :list }
|
|
52
|
-
|
|
53
|
-
case (arg = args.shift)
|
|
54
|
-
when "--page" then options[:page] = Integer(fetch_value!(args, arg))
|
|
55
|
-
when "-h", "--help" then options[:help] = true
|
|
56
|
-
else raise ArgumentError, "unknown option #{arg}"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
69
|
+
parse_flags!(args, options, :list)
|
|
59
70
|
options
|
|
60
71
|
end
|
|
61
72
|
|
|
@@ -83,17 +94,7 @@ module Slidict
|
|
|
83
94
|
end
|
|
84
95
|
|
|
85
96
|
def parse_body_options!(args, options)
|
|
86
|
-
|
|
87
|
-
case (arg = args.shift)
|
|
88
|
-
when "--title" then options[:title] = fetch_value!(args, arg)
|
|
89
|
-
when "--body" then options[:body] = fetch_value!(args, arg)
|
|
90
|
-
when "--file" then options[:file] = fetch_value!(args, arg)
|
|
91
|
-
when "--body-format" then options[:body_format] = fetch_value!(args, arg)
|
|
92
|
-
when "--visibility" then options[:visibility] = fetch_value!(args, arg)
|
|
93
|
-
when "-h", "--help" then options[:help] = true
|
|
94
|
-
else raise ArgumentError, "unknown option #{arg}"
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
+
parse_flags!(args, options, :body_options)
|
|
97
98
|
raise ArgumentError, "specify only one of --body or --file" if options[:body] && options[:file]
|
|
98
99
|
end
|
|
99
100
|
|
|
@@ -111,17 +112,17 @@ module Slidict
|
|
|
111
112
|
def list(options)
|
|
112
113
|
with_reauth_retry do
|
|
113
114
|
print_slide_list(client.list(page: options[:page]))
|
|
114
|
-
|
|
115
|
+
SUCCESS
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
def show(options)
|
|
119
120
|
with_reauth_retry do
|
|
120
121
|
print_slide_detail(client.show(options[:id]))
|
|
121
|
-
|
|
122
|
+
SUCCESS
|
|
122
123
|
rescue External::SlidictIo::Client::NotFound
|
|
123
124
|
@output.puts "Error: slide not found"
|
|
124
|
-
|
|
125
|
+
FAILURE
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
|
|
@@ -147,13 +148,13 @@ module Slidict
|
|
|
147
148
|
slide = yield
|
|
148
149
|
@output.puts "#{verb} slide ##{slide["id"]} (draft)"
|
|
149
150
|
print_slide_detail(slide)
|
|
150
|
-
|
|
151
|
+
SUCCESS
|
|
151
152
|
rescue External::SlidictIo::Client::NotFound
|
|
152
153
|
@output.puts "Error: slide not found"
|
|
153
|
-
|
|
154
|
+
FAILURE
|
|
154
155
|
rescue External::SlidictIo::Client::NotEditable
|
|
155
156
|
@output.puts "Error: this slide is already published. Edit it from the Web UI instead."
|
|
156
|
-
|
|
157
|
+
FAILURE
|
|
157
158
|
rescue External::SlidictIo::Client::RateLimited
|
|
158
159
|
print_rate_limited
|
|
159
160
|
rescue External::SlidictIo::Client::Unprocessable => e
|
|
@@ -230,22 +231,22 @@ module Slidict
|
|
|
230
231
|
|
|
231
232
|
def print_rate_limited
|
|
232
233
|
@output.puts "Error: rate limited. Create/edit is limited to once per minute. Wait and try again."
|
|
233
|
-
|
|
234
|
+
FAILURE
|
|
234
235
|
end
|
|
235
236
|
|
|
236
237
|
def print_unprocessable(error)
|
|
237
238
|
@output.puts "Error: #{error.message}"
|
|
238
239
|
error.errors.each { |message| @output.puts " - #{message}" }
|
|
239
|
-
|
|
240
|
+
FAILURE
|
|
240
241
|
end
|
|
241
242
|
|
|
242
243
|
def print_client_error(error)
|
|
243
244
|
@output.puts "Error: #{error.message}"
|
|
244
|
-
|
|
245
|
+
FAILURE
|
|
245
246
|
end
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
usage do
|
|
249
|
+
<<~USAGE
|
|
249
250
|
Usage: slidict slides <command> [options]
|
|
250
251
|
|
|
251
252
|
Commands:
|
|
@@ -255,16 +256,10 @@ module Slidict
|
|
|
255
256
|
edit <id> [options] Edit an existing draft slide
|
|
256
257
|
|
|
257
258
|
Create/edit options:
|
|
258
|
-
|
|
259
|
-
--body TEXT Slide body text
|
|
260
|
-
--file PATH Read the slide body from a file (instead of --body)
|
|
261
|
-
--body-format FORMAT asciidoc or markdown (default: auto-detected from body)
|
|
262
|
-
--visibility VIS public, unlisted, or group_only (default: public)
|
|
263
|
-
-h, --help Show this help
|
|
259
|
+
#{flags_help(:body_options)}
|
|
264
260
|
|
|
265
261
|
Note: slides are always created/edited as drafts. Publish from the Web UI.
|
|
266
|
-
|
|
267
|
-
0
|
|
262
|
+
USAGE
|
|
268
263
|
end
|
|
269
264
|
end
|
|
270
265
|
end
|
data/lib/slidict/env.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slidict
|
|
4
|
+
# Loads KEY=VALUE pairs from a .env file (if present) into ENV, so
|
|
5
|
+
# SLIDICT_LLM_BASE_URL, SLIDICT_FRAMEWORK, etc. can be set once per project
|
|
6
|
+
# instead of on every invocation. Only keys not already present in ENV are
|
|
7
|
+
# populated -- a real environment variable always wins over .env, and a CLI
|
|
8
|
+
# flag (read afterwards, in Cli::App#parse) wins over both. See `slidict
|
|
9
|
+
# init` for generating a starter file.
|
|
10
|
+
module Env
|
|
11
|
+
LINE = /\A(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)\z/
|
|
12
|
+
|
|
13
|
+
def self.load!(path = ".env", env = ENV)
|
|
14
|
+
return unless File.exist?(path)
|
|
15
|
+
|
|
16
|
+
File.foreach(path) do |line|
|
|
17
|
+
key, value = parse_line(line)
|
|
18
|
+
env[key] = value if key && !env.key?(key)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.parse_line(line)
|
|
23
|
+
line = line.strip
|
|
24
|
+
return [nil, nil] if line.empty? || line.start_with?("#")
|
|
25
|
+
|
|
26
|
+
match = LINE.match(line)
|
|
27
|
+
return [nil, nil] unless match
|
|
28
|
+
|
|
29
|
+
[match[1], unquote(match[2].strip)]
|
|
30
|
+
end
|
|
31
|
+
private_class_method :parse_line
|
|
32
|
+
|
|
33
|
+
# A quoted value keeps everything up to its closing quote verbatim (so a
|
|
34
|
+
# "#" inside it isn't mistaken for a comment); an unquoted value has any
|
|
35
|
+
# trailing "# comment" stripped.
|
|
36
|
+
def self.unquote(value)
|
|
37
|
+
if value.start_with?('"', "'")
|
|
38
|
+
quote = value[0]
|
|
39
|
+
closing = value.index(quote, 1)
|
|
40
|
+
return closing ? value[1...closing] : value[1..]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
value.sub(/\s+#.*\z/, "")
|
|
44
|
+
end
|
|
45
|
+
private_class_method :unquote
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/slidict/llm/client.rb
CHANGED
|
@@ -35,8 +35,8 @@ module Slidict
|
|
|
35
35
|
raise Error, "could not parse models response: #{e.message}"
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def generate_slides(deck)
|
|
39
|
-
content = chat_completion(prompt_for(deck))
|
|
38
|
+
def generate_slides(deck, language: nil)
|
|
39
|
+
content = chat_completion(prompt_for(deck, language: language))
|
|
40
40
|
slides_from(content)
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -48,6 +48,16 @@ module Slidict
|
|
|
48
48
|
findings_from(content)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Translates a single line of text into language. Unlike
|
|
52
|
+
# generate_slides/lint_slides, the response is a plain string rather
|
|
53
|
+
# than parsed JSON: asking a (often small, local) model to translate
|
|
54
|
+
# one line is a task it can follow reliably, whereas asking it to also
|
|
55
|
+
# return a JSON array of a specific length is exactly the kind of
|
|
56
|
+
# structured-output instruction weaker models tend to get wrong.
|
|
57
|
+
def translate_text(text, language)
|
|
58
|
+
strip_quotes(chat_completion(translate_prompt_for(text, language)).strip)
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
private
|
|
52
62
|
|
|
53
63
|
def get_models_response
|
|
@@ -57,7 +67,7 @@ module Slidict
|
|
|
57
67
|
perform_request(uri, request)
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
def prompt_for(deck)
|
|
70
|
+
def prompt_for(deck, language: nil)
|
|
61
71
|
<<~PROMPT
|
|
62
72
|
You are an assistant that designs presentation slide outlines.
|
|
63
73
|
Topic: #{deck.topic}
|
|
@@ -69,11 +79,17 @@ module Slidict
|
|
|
69
79
|
Return one slide for each required slide role when a presentation method is
|
|
70
80
|
provided; otherwise return exactly 5 slides. Each item must be an object with
|
|
71
81
|
a "title" string and a "bullets" array of 2-4 short strings.
|
|
72
|
-
Respond with the JSON array only: no commentary, no markdown code
|
|
73
|
-
and no reasoning or thinking content before or after it.
|
|
82
|
+
#{language_instruction_for(language)}Respond with the JSON array only: no commentary, no markdown code
|
|
83
|
+
fences, and no reasoning or thinking content before or after it.
|
|
74
84
|
PROMPT
|
|
75
85
|
end
|
|
76
86
|
|
|
87
|
+
def language_instruction_for(language)
|
|
88
|
+
return "" unless language
|
|
89
|
+
|
|
90
|
+
"Write the \"title\" and \"bullets\" text in #{language}.\n"
|
|
91
|
+
end
|
|
92
|
+
|
|
77
93
|
def method_prompt_for(deck)
|
|
78
94
|
method = deck.presentation_method
|
|
79
95
|
return "" unless method
|
|
@@ -132,6 +148,23 @@ module Slidict
|
|
|
132
148
|
translate ? "#{prompt}\nTranslate only the \"message\" field of each finding into #{translate}. Keep \"slide\" as an integer and \"severity\" as exactly \"warning\" or \"info\" — do not translate those values." : prompt
|
|
133
149
|
end
|
|
134
150
|
|
|
151
|
+
def translate_prompt_for(text, language)
|
|
152
|
+
<<~PROMPT
|
|
153
|
+
Translate the following text into #{language}. Respond with only the
|
|
154
|
+
translation: no commentary, no surrounding quotation marks, no
|
|
155
|
+
markdown formatting, and no reasoning or thinking content before or
|
|
156
|
+
after it.
|
|
157
|
+
|
|
158
|
+
#{text}
|
|
159
|
+
PROMPT
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Small/local models often ignore the "no quotation marks" instruction
|
|
163
|
+
# above and wrap the translation anyway.
|
|
164
|
+
def strip_quotes(text)
|
|
165
|
+
text.sub(/\A["'“”‘’「」]+/, "").sub(/["'“”‘’「」]+\z/, "")
|
|
166
|
+
end
|
|
167
|
+
|
|
135
168
|
def findings_from(content)
|
|
136
169
|
parsed = JSON.parse(extract_json_array(content))
|
|
137
170
|
raise Error, "expected a JSON array of findings" unless parsed.is_a?(Array)
|
data/lib/slidict/version.rb
CHANGED
data/lib/slidict.rb
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
|
+
require_relative "slidict/cli/options"
|
|
5
6
|
require_relative "slidict/cli/app"
|
|
6
7
|
require_relative "slidict/cli/lint"
|
|
7
8
|
require_relative "slidict/cli/serve"
|
|
8
9
|
require_relative "slidict/cli/slides"
|
|
9
10
|
require_relative "slidict/config"
|
|
10
11
|
require_relative "slidict/deck"
|
|
12
|
+
require_relative "slidict/env"
|
|
11
13
|
require_relative "slidict/external/slidict_io/auth"
|
|
12
14
|
require_relative "slidict/external/slidict_io/client"
|
|
13
15
|
require_relative "slidict/external/slidict_io/credentials"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slidict
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Abe
|
|
@@ -83,10 +83,12 @@ files:
|
|
|
83
83
|
- lib/slidict.rb
|
|
84
84
|
- lib/slidict/cli/app.rb
|
|
85
85
|
- lib/slidict/cli/lint.rb
|
|
86
|
+
- lib/slidict/cli/options.rb
|
|
86
87
|
- lib/slidict/cli/serve.rb
|
|
87
88
|
- lib/slidict/cli/slides.rb
|
|
88
89
|
- lib/slidict/config.rb
|
|
89
90
|
- lib/slidict/deck.rb
|
|
91
|
+
- lib/slidict/env.rb
|
|
90
92
|
- lib/slidict/external/slidict_io/auth.rb
|
|
91
93
|
- lib/slidict/external/slidict_io/client.rb
|
|
92
94
|
- lib/slidict/external/slidict_io/credentials.rb
|