prompt_objects 0.2.0 → 0.3.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +2 -2
  5. data/exe/prompt_objects +548 -1
  6. data/frontend/src/App.tsx +11 -3
  7. data/frontend/src/components/ContextMenu.tsx +67 -0
  8. data/frontend/src/components/MessageBus.tsx +4 -3
  9. data/frontend/src/components/ModelSelector.tsx +5 -1
  10. data/frontend/src/components/ThreadsSidebar.tsx +46 -2
  11. data/frontend/src/components/UsagePanel.tsx +105 -0
  12. data/frontend/src/hooks/useWebSocket.ts +53 -0
  13. data/frontend/src/store/index.ts +10 -0
  14. data/frontend/src/types/index.ts +4 -1
  15. data/lib/prompt_objects/cli.rb +1 -0
  16. data/lib/prompt_objects/connectors/mcp.rb +1 -0
  17. data/lib/prompt_objects/environment.rb +24 -1
  18. data/lib/prompt_objects/llm/anthropic_adapter.rb +15 -1
  19. data/lib/prompt_objects/llm/factory.rb +93 -6
  20. data/lib/prompt_objects/llm/gemini_adapter.rb +13 -1
  21. data/lib/prompt_objects/llm/openai_adapter.rb +21 -4
  22. data/lib/prompt_objects/llm/pricing.rb +49 -0
  23. data/lib/prompt_objects/llm/response.rb +3 -2
  24. data/lib/prompt_objects/mcp/server.rb +1 -0
  25. data/lib/prompt_objects/message_bus.rb +27 -8
  26. data/lib/prompt_objects/prompt_object.rb +6 -4
  27. data/lib/prompt_objects/server/api/routes.rb +186 -29
  28. data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +1 -0
  29. data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +77 -0
  30. data/lib/prompt_objects/server/public/index.html +2 -2
  31. data/lib/prompt_objects/server/websocket_handler.rb +93 -9
  32. data/lib/prompt_objects/server.rb +54 -0
  33. data/lib/prompt_objects/session/store.rb +399 -4
  34. data/lib/prompt_objects.rb +1 -0
  35. data/prompt_objects.gemspec +1 -1
  36. data/templates/arc-agi-1/manifest.yml +22 -0
  37. data/templates/arc-agi-1/objects/data_manager.md +42 -0
  38. data/templates/arc-agi-1/objects/observer.md +100 -0
  39. data/templates/arc-agi-1/objects/solver.md +118 -0
  40. data/templates/arc-agi-1/objects/verifier.md +79 -0
  41. data/templates/arc-agi-1/primitives/check_arc_data.rb +53 -0
  42. data/templates/arc-agi-1/primitives/find_objects.rb +72 -0
  43. data/templates/arc-agi-1/primitives/grid_diff.rb +70 -0
  44. data/templates/arc-agi-1/primitives/grid_info.rb +42 -0
  45. data/templates/arc-agi-1/primitives/grid_transform.rb +50 -0
  46. data/templates/arc-agi-1/primitives/load_arc_task.rb +68 -0
  47. data/templates/arc-agi-1/primitives/render_grid.rb +78 -0
  48. data/templates/arc-agi-1/primitives/test_solution.rb +131 -0
  49. data/tools/thread-explorer.html +1043 -0
  50. metadata +21 -3
  51. data/lib/prompt_objects/server/public/assets/index-CeNJvqLG.js +0 -77
  52. data/lib/prompt_objects/server/public/assets/index-Vx4-uMOU.css +0 -1
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module LLM
5
+ # Static pricing table for cost estimation.
6
+ # Prices are per 1 million tokens in USD.
7
+ # Updated periodically — not guaranteed to be exact.
8
+ class Pricing
9
+ RATES = {
10
+ # OpenAI
11
+ "gpt-5.2" => { input: 2.00, output: 8.00 },
12
+ "gpt-4.1" => { input: 2.00, output: 8.00 },
13
+ "gpt-4.1-mini" => { input: 0.40, output: 1.60 },
14
+ "gpt-4.5-preview" => { input: 75.00, output: 150.00 },
15
+ "o3-mini" => { input: 1.10, output: 4.40 },
16
+ "o1" => { input: 15.00, output: 60.00 },
17
+ # Anthropic
18
+ "claude-opus-4" => { input: 15.00, output: 75.00 },
19
+ "claude-sonnet-4-5" => { input: 3.00, output: 15.00 },
20
+ "claude-haiku-4-5" => { input: 1.00, output: 5.00 },
21
+ # Gemini
22
+ "gemini-3-flash-preview" => { input: 0.15, output: 0.60 },
23
+ "gemini-2.5-pro" => { input: 1.25, output: 10.00 },
24
+ "gemini-2.5-flash" => { input: 0.15, output: 0.60 },
25
+ }.freeze
26
+
27
+ # Calculate cost in USD for a given usage.
28
+ # @param model [String] Model name
29
+ # @param input_tokens [Integer] Number of input tokens
30
+ # @param output_tokens [Integer] Number of output tokens
31
+ # @return [Float] Estimated cost in USD
32
+ def self.calculate(model:, input_tokens:, output_tokens:)
33
+ rates = RATES[model]
34
+ return 0.0 unless rates
35
+
36
+ input_cost = (input_tokens / 1_000_000.0) * rates[:input]
37
+ output_cost = (output_tokens / 1_000_000.0) * rates[:output]
38
+ input_cost + output_cost
39
+ end
40
+
41
+ # Check if we have pricing data for a model.
42
+ # @param model [String] Model name
43
+ # @return [Boolean]
44
+ def self.known_model?(model)
45
+ RATES.key?(model)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -5,12 +5,13 @@ module PromptObjects
5
5
  # Normalized response from an LLM API call.
6
6
  # Wraps provider-specific responses into a common interface.
7
7
  class Response
8
- attr_reader :content, :tool_calls, :raw
8
+ attr_reader :content, :tool_calls, :raw, :usage
9
9
 
10
- def initialize(content:, tool_calls: [], raw: nil)
10
+ def initialize(content:, tool_calls: [], raw: nil, usage: nil)
11
11
  @content = content
12
12
  @tool_calls = tool_calls
13
13
  @raw = raw
14
+ @usage = usage # { input_tokens:, output_tokens:, model:, provider: }
14
15
  end
15
16
 
16
17
  # Check if the response includes tool calls.
@@ -138,6 +138,7 @@ module PromptObjects
138
138
  {
139
139
  from: entry[:from],
140
140
  to: entry[:to],
141
+ summary: entry[:summary],
141
142
  message: entry[:message],
142
143
  timestamp: entry[:timestamp]&.iso8601
143
144
  }
@@ -4,28 +4,36 @@ module PromptObjects
4
4
  # Message bus for routing and logging all inter-capability communication.
5
5
  # This makes the semantic binding visible - you can see natural language
6
6
  # being transformed into capability calls.
7
+ #
8
+ # Each entry stores the full message content and a truncated summary.
9
+ # Use :summary for compact log displays, :message for full inspection.
7
10
  class MessageBus
8
11
  attr_reader :log
9
12
 
10
- def initialize
13
+ # @param session_store [Session::Store, nil] Optional store for persistent event logging
14
+ def initialize(session_store: nil)
11
15
  @log = []
12
16
  @subscribers = []
17
+ @store = session_store
13
18
  end
14
19
 
15
20
  # Log a message between capabilities.
16
21
  # @param from [String] Source capability name
17
22
  # @param to [String] Destination capability name
18
- # @param message [String, Hash] The message content
23
+ # @param message [String, Hash] The message content (stored in full)
24
+ # @param session_id [String, nil] Optional session ID for event persistence
19
25
  # @return [Hash] The log entry
20
- def publish(from:, to:, message:)
26
+ def publish(from:, to:, message:, session_id: nil)
21
27
  entry = {
22
28
  timestamp: Time.now,
23
29
  from: from,
24
30
  to: to,
25
- message: truncate_message(message)
31
+ message: message,
32
+ summary: summarize(message)
26
33
  }
27
34
 
28
35
  @log << entry
36
+ persist_event(entry, session_id: session_id)
29
37
  notify_subscribers(entry)
30
38
  entry
31
39
  end
@@ -54,7 +62,7 @@ module PromptObjects
54
62
  @log.clear
55
63
  end
56
64
 
57
- # Format log entries for display.
65
+ # Format log entries for compact display.
58
66
  # @param count [Integer] Number of entries to format
59
67
  # @return [String]
60
68
  def format_log(count = 20)
@@ -62,7 +70,7 @@ module PromptObjects
62
70
  time = entry[:timestamp].strftime("%H:%M:%S")
63
71
  from = entry[:from]
64
72
  to = entry[:to]
65
- msg = entry[:message]
73
+ msg = entry[:summary]
66
74
 
67
75
  "#{time} #{from} → #{to}: #{msg}"
68
76
  end.join("\n")
@@ -74,7 +82,18 @@ module PromptObjects
74
82
  @subscribers.each { |s| s.call(entry) }
75
83
  end
76
84
 
77
- def truncate_message(message, max_length = 100)
85
+ # Persist event to the session store (if available).
86
+ def persist_event(entry, session_id: nil)
87
+ return unless @store
88
+
89
+ @store.add_event(entry, session_id: session_id)
90
+ rescue StandardError => e
91
+ # Don't let persistence failures break the bus
92
+ warn "Warning: Failed to persist event: #{e.message}"
93
+ end
94
+
95
+ # Create a short summary for compact log displays.
96
+ def summarize(message, max_length = 200)
78
97
  str = case message
79
98
  when Hash
80
99
  message.to_json
@@ -84,7 +103,7 @@ module PromptObjects
84
103
  message.to_s
85
104
  end
86
105
 
87
- # Remove newlines for cleaner log display
106
+ # Collapse whitespace for single-line display
88
107
  str = str.gsub(/\s+/, " ").strip
89
108
 
90
109
  if str.length > max_length
@@ -63,7 +63,7 @@ module PromptObjects
63
63
  content = normalize_message(message)
64
64
 
65
65
  # Track who sent this message - another PO or a human?
66
- sender = context.current_capability
66
+ sender = context.calling_po
67
67
  from = (sender && sender != name) ? sender : "human"
68
68
 
69
69
  user_msg = { role: :user, content: content, from: from }
@@ -88,7 +88,8 @@ module PromptObjects
88
88
  # wait for tool results before generating a response. This prevents
89
89
  # the model from "hedging" by generating both a response AND a tool call.
90
90
  content: nil,
91
- tool_calls: response.tool_calls
91
+ tool_calls: response.tool_calls,
92
+ usage: response.usage
92
93
  }
93
94
  @history << assistant_msg
94
95
  persist_message(assistant_msg)
@@ -101,7 +102,7 @@ module PromptObjects
101
102
  notify_history_updated
102
103
  else
103
104
  # No tool calls - we have our final response
104
- assistant_msg = { role: :assistant, content: response.content }
105
+ assistant_msg = { role: :assistant, content: response.content, usage: response.usage }
105
106
  @history << assistant_msg
106
107
  persist_message(assistant_msg)
107
108
  @state = :idle
@@ -390,7 +391,8 @@ module PromptObjects
390
391
  session_id: @session_id,
391
392
  role: :assistant,
392
393
  content: msg[:content],
393
- tool_calls: tool_calls_data
394
+ tool_calls: tool_calls_data,
395
+ usage: msg[:usage]
394
396
  )
395
397
  when :tool
396
398
  session_store.add_message(
@@ -28,40 +28,53 @@ module PromptObjects
28
28
  def route(request, path)
29
29
  method = request.request_method
30
30
 
31
- case [method, path]
31
+ case method
32
+ when "GET"
33
+ route_get(request, path)
34
+ when "POST"
35
+ route_post(request, path)
36
+ else
37
+ { error: "Not found", path: path }
38
+ end
39
+ end
32
40
 
33
- # Environment
34
- when ["GET", "/environment"]
41
+ def route_get(_request, path)
42
+ case path
43
+ when "/environment"
35
44
  get_environment
36
-
37
- # Prompt Objects
38
- when ["GET", "/prompt_objects"]
45
+ when "/prompt_objects"
39
46
  list_prompt_objects
40
-
41
- when ["GET", %r{^/prompt_objects/([^/]+)$}]
42
- get_prompt_object(path_param(path, 1))
43
-
44
- when ["POST", "/prompt_objects"]
45
- create_prompt_object(request.body.read)
46
-
47
- # Sessions
48
- when ["GET", %r{^/prompt_objects/([^/]+)/sessions$}]
49
- list_sessions(path_param(path, 1))
50
-
51
- when ["GET", %r{^/prompt_objects/([^/]+)/sessions/([^/]+)$}]
52
- get_session(path_param(path, 1), path_param(path, 2))
53
-
54
- when ["POST", %r{^/prompt_objects/([^/]+)/sessions$}]
55
- create_session(path_param(path, 1), request.body.read)
56
-
57
- # Primitives
58
- when ["GET", "/primitives"]
47
+ when "/primitives"
59
48
  list_primitives
60
-
61
- # Message Bus
62
- when ["GET", "/bus/recent"]
49
+ when "/bus/recent"
63
50
  get_recent_bus_messages
51
+ when "/events"
52
+ get_recent_events(_request)
53
+ when %r{^/prompt_objects/([^/]+)/sessions/([^/]+)$}
54
+ get_session($1, $2)
55
+ when %r{^/prompt_objects/([^/]+)/sessions$}
56
+ list_sessions($1)
57
+ when %r{^/prompt_objects/([^/]+)$}
58
+ get_prompt_object($1)
59
+ when %r{^/events/session/([^/]+)$}
60
+ get_session_events($1)
61
+ when %r{^/sessions/([^/]+)/usage$}
62
+ get_session_usage($1, _request)
63
+ when %r{^/sessions/([^/]+)/export$}
64
+ export_thread($1, _request)
65
+ else
66
+ { error: "Not found", path: path }
67
+ end
68
+ end
64
69
 
70
+ def route_post(request, path)
71
+ case path
72
+ when "/prompt_objects"
73
+ create_prompt_object(request.body.read)
74
+ when %r{^/prompt_objects/([^/]+)/message$}
75
+ send_message($1, request.body.read)
76
+ when %r{^/prompt_objects/([^/]+)/sessions$}
77
+ create_session($1, request.body.read)
65
78
  else
66
79
  { error: "Not found", path: path }
67
80
  end
@@ -114,6 +127,61 @@ module PromptObjects
114
127
  { error: "Not implemented" }
115
128
  end
116
129
 
130
+ # === Messages ===
131
+
132
+ def send_message(po_name, body)
133
+ po = @runtime.registry.get(po_name)
134
+
135
+ unless po.is_a?(PromptObject)
136
+ return { error: "Prompt object not found", name: po_name }
137
+ end
138
+
139
+ params = JSON.parse(body)
140
+ message = params["message"]
141
+ session_id = params["session_id"]
142
+ new_thread = params["new_thread"]
143
+
144
+ return { error: "Message is required" } unless message && !message.empty?
145
+
146
+ # Create a new thread if requested
147
+ if new_thread
148
+ session_id = po.new_thread
149
+ end
150
+
151
+ # Switch to specified session if provided
152
+ if session_id && session_id != po.session_id
153
+ po.switch_session(session_id)
154
+ end
155
+
156
+ request_session_id = po.session_id
157
+
158
+ # Send the message through the same path as WebSocket
159
+ context = @runtime.context(tui_mode: false)
160
+ context.current_capability = "human"
161
+
162
+ # Log to bus
163
+ @runtime.bus.publish(from: "human", to: po.name, message: message, session_id: request_session_id)
164
+
165
+ response = po.receive(message, context: context)
166
+
167
+ # Log response to bus
168
+ @runtime.bus.publish(from: po.name, to: "human", message: response, session_id: request_session_id)
169
+
170
+ # Count events for this session
171
+ event_count = if @runtime.session_store
172
+ @runtime.session_store.get_events(session_id: request_session_id).length
173
+ end
174
+
175
+ {
176
+ response: response,
177
+ po_name: po.name,
178
+ session_id: request_session_id,
179
+ event_count: event_count
180
+ }
181
+ rescue JSON::ParserError
182
+ { error: "Invalid JSON body" }
183
+ end
184
+
117
185
  # === Sessions ===
118
186
 
119
187
  def list_sessions(po_name)
@@ -204,7 +272,8 @@ module PromptObjects
204
272
  {
205
273
  from: e[:from],
206
274
  to: e[:to],
207
- message: e[:message],
275
+ summary: e[:summary],
276
+ content: serialize_bus_content(e[:message]),
208
277
  timestamp: e[:timestamp].iso8601
209
278
  }
210
279
  end
@@ -212,6 +281,94 @@ module PromptObjects
212
281
  { messages: messages }
213
282
  end
214
283
 
284
+ def serialize_bus_content(message)
285
+ case message
286
+ when Hash then message
287
+ when String then message
288
+ else message.to_s
289
+ end
290
+ end
291
+
292
+ # === Events ===
293
+
294
+ def get_recent_events(request)
295
+ return { error: "No session store" } unless @runtime.session_store
296
+
297
+ count = (request.params["count"] || 50).to_i
298
+ events = @runtime.session_store.get_recent_events(count)
299
+
300
+ { events: events.map { |e| format_event(e) } }
301
+ end
302
+
303
+ def get_session_events(session_id)
304
+ return { error: "No session store" } unless @runtime.session_store
305
+
306
+ events = @runtime.session_store.get_events(session_id: session_id)
307
+
308
+ { events: events.map { |e| format_event(e) } }
309
+ end
310
+
311
+ def format_event(event)
312
+ {
313
+ id: event[:id],
314
+ session_id: event[:session_id],
315
+ from: event[:from],
316
+ to: event[:to],
317
+ summary: event[:summary],
318
+ message: event[:message],
319
+ timestamp: event[:timestamp]&.iso8601
320
+ }
321
+ end
322
+
323
+ # === Usage ===
324
+
325
+ def get_session_usage(session_id, request)
326
+ return { error: "No session store" } unless @runtime.session_store
327
+
328
+ include_tree = request.params["tree"] == "true"
329
+
330
+ usage = if include_tree
331
+ @runtime.session_store.thread_tree_usage(session_id)
332
+ else
333
+ @runtime.session_store.session_usage(session_id)
334
+ end
335
+
336
+ by_model = {}
337
+ usage[:by_model].each { |model, data| by_model[model.to_s] = data }
338
+
339
+ {
340
+ session_id: session_id,
341
+ include_tree: include_tree,
342
+ input_tokens: usage[:input_tokens],
343
+ output_tokens: usage[:output_tokens],
344
+ total_tokens: usage[:total_tokens],
345
+ estimated_cost_usd: usage[:estimated_cost_usd].round(6),
346
+ calls: usage[:calls],
347
+ by_model: by_model
348
+ }
349
+ end
350
+
351
+ # === Export ===
352
+
353
+ def export_thread(session_id, request)
354
+ return { error: "No session store" } unless @runtime.session_store
355
+
356
+ format = request.params["format"] || "markdown"
357
+
358
+ case format
359
+ when "markdown"
360
+ content = @runtime.session_store.export_thread_tree_markdown(session_id)
361
+ return { error: "Session not found" } unless content
362
+ { format: "markdown", content: content }
363
+ when "json"
364
+ data = @runtime.session_store.export_thread_tree_json(session_id)
365
+ return { error: "Session not found" } unless data
366
+ data
367
+ else
368
+ { error: "Unknown format: #{format}" }
369
+ end
370
+ end
371
+
215
372
  # === Helpers ===
216
373
 
217
374
  def po_summary(po)
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-4{bottom:1rem}.left-2{left:.5rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-16{top:4rem}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-11{margin-left:2.75rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-96{height:24rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-64{max-height:16rem}.max-h-\[60vh\]{max-height:60vh}.max-h-\[80vh\]{max-height:80vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-\[420px\]{width:420px}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-full{min-width:100%}.max-w-\[80\%\]{max-width:80%}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-po-accent{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.border-po-border{--tw-border-opacity: 1;border-color:rgb(45 45 68 / var(--tw-border-opacity, 1))}.border-po-border\/30{border-color:#2d2d444d}.border-po-border\/50{border-color:#2d2d4480}.border-po-warning\/30{border-color:#f59e0b4d}.border-transparent{border-color:transparent}.bg-black\/50{background-color:#00000080}.bg-blue-600\/30{background-color:#2563eb4d}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-600\/30{background-color:#4b55634d}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-po-accent{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.bg-po-accent\/20{background-color:#7c3aed33}.bg-po-bg{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.bg-po-bg\/50{background-color:#0f0f1a80}.bg-po-bg\/80{background-color:#0f0f1acc}.bg-po-border{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.bg-po-surface{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.bg-po-surface\/50{background-color:#1a1a2e80}.bg-po-warning{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-po-warning\/10{background-color:#f59e0b1a}.bg-purple-600\/30{background-color:#9333ea4d}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.text-po-accent\/70{color:#7c3aedb3}.text-po-warning{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}html{color-scheme:dark}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0px}.hover\:border-po-accent:hover{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.hover\:border-po-accent\/50:hover{border-color:#7c3aed80}.hover\:bg-po-accent\/50:hover{background-color:#7c3aed80}.hover\:bg-po-accent\/80:hover{background-color:#7c3aedcc}.hover\:bg-po-bg:hover{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.hover\:bg-po-bg\/70:hover{background-color:#0f0f1ab3}.hover\:bg-po-border:hover{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface:hover{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.hover\:bg-po-warning\/20:hover{background-color:#f59e0b33}.hover\:text-po-accent:hover{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-po-accent:focus{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-po-accent:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(124 58 237 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}