glancer 1.0.0

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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +96 -0
  3. data/.rubocop.yml +54 -0
  4. data/CHANGELOG.md +88 -0
  5. data/CLAUDE.md +115 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/README.md +354 -0
  8. data/app/assets/config/glancer_manifest.js +1 -0
  9. data/app/assets/javascripts/glancer/application.js +15 -0
  10. data/app/assets/javascripts/glancer/controllers/chat_controller.js +101 -0
  11. data/app/assets/javascripts/glancer/controllers/message_controller.js +1052 -0
  12. data/app/assets/javascripts/glancer/controllers/toast_controller.js +63 -0
  13. data/app/assets/stylesheets/glancer/application.css +350 -0
  14. data/app/assets/stylesheets/glancer/code-blocks.css +6 -0
  15. data/app/assets/stylesheets/glancer/list.css +31 -0
  16. data/app/assets/stylesheets/glancer/scrollbar.css +16 -0
  17. data/app/assets/stylesheets/glancer/table.css +97 -0
  18. data/app/controllers/glancer/application_controller.rb +33 -0
  19. data/app/controllers/glancer/chats_controller.rb +49 -0
  20. data/app/controllers/glancer/messages_controller.rb +144 -0
  21. data/app/controllers/glancer/schema_controller.rb +29 -0
  22. data/app/controllers/glancer/settings_controller.rb +23 -0
  23. data/app/helpers/glancer/application_helper.rb +17 -0
  24. data/app/jobs/glancer/application_job.rb +6 -0
  25. data/app/jobs/glancer/process_message_job.rb +38 -0
  26. data/app/models/glancer/audit.rb +12 -0
  27. data/app/models/glancer/chat.rb +8 -0
  28. data/app/models/glancer/code_version.rb +12 -0
  29. data/app/models/glancer/embedding.rb +6 -0
  30. data/app/models/glancer/message.rb +25 -0
  31. data/app/models/glancer/setting.rb +23 -0
  32. data/app/models/glancer/sql_version.rb +6 -0
  33. data/app/views/glancer/_data/_importmap.json.erb +7 -0
  34. data/app/views/glancer/chats/_chat_sidebar.html.erb +2 -0
  35. data/app/views/glancer/chats/_show.html.erb +52 -0
  36. data/app/views/glancer/chats/_sidebar_chat_list.html.erb +30 -0
  37. data/app/views/glancer/chats/index.html.erb +10 -0
  38. data/app/views/glancer/chats/show.html.erb +1 -0
  39. data/app/views/glancer/messages/_data_table.html.erb +268 -0
  40. data/app/views/glancer/messages/_execution_error.html.erb +26 -0
  41. data/app/views/glancer/messages/_form.html.erb +93 -0
  42. data/app/views/glancer/messages/_message.html.erb +206 -0
  43. data/app/views/glancer/messages/_message_info.html.erb +176 -0
  44. data/app/views/glancer/messages/_temp_form.html.erb +100 -0
  45. data/app/views/glancer/messages/create.turbo_stream.erb +25 -0
  46. data/app/views/glancer/schema/show.html.erb +123 -0
  47. data/app/views/glancer/settings/show.html.erb +306 -0
  48. data/app/views/glancer/shared/_icons.html.erb +126 -0
  49. data/app/views/layouts/glancer/application.html.erb +234 -0
  50. data/config/locales/glancer.en.yml +90 -0
  51. data/config/locales/glancer.es.yml +90 -0
  52. data/config/locales/glancer.pt-BR.yml +90 -0
  53. data/config/routes.rb +20 -0
  54. data/db/migrate/20250629212642_create_glancer_audits.rb +19 -0
  55. data/db/migrate/20250629212643_create_glancer_chats.rb +10 -0
  56. data/db/migrate/20250629212645_create_glancer_embeddings.rb +17 -0
  57. data/db/migrate/20250629212647_create_glancer_messages.rb +29 -0
  58. data/db/migrate/20260513204129_add_user_edited_sql_to_glancer_messages.rb +11 -0
  59. data/db/migrate/20260513210647_create_glancer_sql_versions.rb +18 -0
  60. data/db/migrate/20260513210648_add_message_id_to_glancer_audits.rb +8 -0
  61. data/db/migrate/20260513220000_create_glancer_settings.rb +12 -0
  62. data/db/migrate/20260514083509_add_llm_model_to_glancer_messages.rb +9 -0
  63. data/db/migrate/20260523120000_rename_code_columns_in_glancer_messages.rb +8 -0
  64. data/db/migrate/20260523120001_rename_code_column_in_glancer_audits.rb +7 -0
  65. data/db/migrate/20260523120002_add_code_type_to_glancer_tables.rb +10 -0
  66. data/db/migrate/20260523120003_rename_glancer_sql_versions_to_code_versions.rb +8 -0
  67. data/db/migrate/20260523130000_add_enriched_question_to_glancer_messages.rb +7 -0
  68. data/db/migrate/20260524100000_add_status_to_glancer_messages.rb +9 -0
  69. data/lib/generators/glancer/install/install_generator.rb +74 -0
  70. data/lib/generators/glancer/install/templates/glancer.rb +227 -0
  71. data/lib/generators/glancer/install/templates/llm_context.glancer.md +51 -0
  72. data/lib/glancer/async_runner.rb +50 -0
  73. data/lib/glancer/chart_analyzer.rb +230 -0
  74. data/lib/glancer/configuration.rb +372 -0
  75. data/lib/glancer/engine.rb +90 -0
  76. data/lib/glancer/indexer/context_indexer.rb +58 -0
  77. data/lib/glancer/indexer/model_indexer.rb +64 -0
  78. data/lib/glancer/indexer/schema_indexer.rb +171 -0
  79. data/lib/glancer/indexer.rb +50 -0
  80. data/lib/glancer/retriever.rb +114 -0
  81. data/lib/glancer/utils/logger.rb +83 -0
  82. data/lib/glancer/utils/markdown_helper.rb +56 -0
  83. data/lib/glancer/utils/result_formatter.rb +25 -0
  84. data/lib/glancer/utils/table_stats.rb +18 -0
  85. data/lib/glancer/utils/transaction.rb +59 -0
  86. data/lib/glancer/version.rb +5 -0
  87. data/lib/glancer/workflow/ar_executor.rb +104 -0
  88. data/lib/glancer/workflow/ar_extractor.rb +25 -0
  89. data/lib/glancer/workflow/ar_prompt_builder.rb +64 -0
  90. data/lib/glancer/workflow/ar_sanitizer.rb +88 -0
  91. data/lib/glancer/workflow/builder.rb +129 -0
  92. data/lib/glancer/workflow/cache.rb +55 -0
  93. data/lib/glancer/workflow/executor.rb +72 -0
  94. data/lib/glancer/workflow/llm.rb +123 -0
  95. data/lib/glancer/workflow/prompt_builder.rb +143 -0
  96. data/lib/glancer/workflow/query_enricher.rb +117 -0
  97. data/lib/glancer/workflow/sql_extractor.rb +42 -0
  98. data/lib/glancer/workflow/sql_sanitizer.rb +42 -0
  99. data/lib/glancer/workflow/sql_validator.rb +67 -0
  100. data/lib/glancer/workflow.rb +158 -0
  101. data/lib/glancer.rb +50 -0
  102. data/lib/tasks/glancer/tailwind.rake +8 -0
  103. data/lib/tasks/glancer.rake +99 -0
  104. data/spec/glancer_spec.rb +62 -0
  105. data/spec/lib/glancer/async_runner_spec.rb +133 -0
  106. data/spec/lib/glancer/chart_analyzer_spec.rb +296 -0
  107. data/spec/lib/glancer/configuration_spec.rb +858 -0
  108. data/spec/lib/glancer/engine_spec.rb +209 -0
  109. data/spec/lib/glancer/indexer/context_indexer_spec.rb +96 -0
  110. data/spec/lib/glancer/indexer/model_indexer_spec.rb +103 -0
  111. data/spec/lib/glancer/indexer/schema_indexer_spec.rb +382 -0
  112. data/spec/lib/glancer/indexer_spec.rb +95 -0
  113. data/spec/lib/glancer/retriever_spec.rb +179 -0
  114. data/spec/lib/glancer/utils/logger_spec.rb +85 -0
  115. data/spec/lib/glancer/utils/markdown_helper_spec.rb +92 -0
  116. data/spec/lib/glancer/utils/result_formatter_spec.rb +73 -0
  117. data/spec/lib/glancer/utils/table_stats_spec.rb +34 -0
  118. data/spec/lib/glancer/utils/transaction_spec.rb +73 -0
  119. data/spec/lib/glancer/workflow/ar_executor_spec.rb +155 -0
  120. data/spec/lib/glancer/workflow/ar_extractor_spec.rb +50 -0
  121. data/spec/lib/glancer/workflow/ar_prompt_builder_spec.rb +79 -0
  122. data/spec/lib/glancer/workflow/ar_sanitizer_spec.rb +175 -0
  123. data/spec/lib/glancer/workflow/builder_spec.rb +204 -0
  124. data/spec/lib/glancer/workflow/cache_spec.rb +142 -0
  125. data/spec/lib/glancer/workflow/executor_spec.rb +149 -0
  126. data/spec/lib/glancer/workflow/llm_spec.rb +124 -0
  127. data/spec/lib/glancer/workflow/prompt_builder_spec.rb +196 -0
  128. data/spec/lib/glancer/workflow/query_enricher_spec.rb +184 -0
  129. data/spec/lib/glancer/workflow/sql_extractor_spec.rb +82 -0
  130. data/spec/lib/glancer/workflow/sql_sanitizer_spec.rb +98 -0
  131. data/spec/lib/glancer/workflow/sql_validator_spec.rb +166 -0
  132. data/spec/lib/glancer/workflow_spec.rb +308 -0
  133. data/spec/models/glancer/audit_spec.rb +82 -0
  134. data/spec/models/glancer/chat_spec.rb +60 -0
  135. data/spec/models/glancer/code_version_spec.rb +71 -0
  136. data/spec/models/glancer/embedding_spec.rb +73 -0
  137. data/spec/models/glancer/message_spec.rb +144 -0
  138. data/spec/models/glancer/setting_spec.rb +88 -0
  139. data/spec/models/glancer/sql_version_spec.rb +4 -0
  140. data/spec/spec_helper.rb +128 -0
  141. data/spec/support/schema.rb +55 -0
  142. metadata +255 -0
@@ -0,0 +1,63 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ document.addEventListener("glancer:toast", this.handleToast.bind(this));
6
+ }
7
+
8
+ disconnect() {
9
+ document.removeEventListener("glancer:toast", this.handleToast.bind(this));
10
+ }
11
+
12
+ handleToast(event) {
13
+ const { message, type = "info" } = event.detail;
14
+ this.show(message, type);
15
+ }
16
+
17
+ show(message, type = "info") {
18
+ const toast = document.createElement("div");
19
+
20
+ const colors = {
21
+ success: "bg-green-600 text-white",
22
+ error: "bg-red-600 text-white",
23
+ info: "bg-gray-800 dark:bg-gray-700 text-white",
24
+ };
25
+
26
+ const icons = {
27
+ success: "#icon-check",
28
+ error: "#icon-x",
29
+ info: "#icon-info",
30
+ };
31
+
32
+ toast.className = [
33
+ "pointer-events-auto flex items-center gap-2.5 px-4 py-3 rounded-xl shadow-lg text-sm font-medium",
34
+ "translate-y-2 opacity-0 transition-all duration-200 ease-out",
35
+ colors[type] || colors.info,
36
+ ].join(" ");
37
+
38
+ toast.innerHTML = `
39
+ <svg class="w-4 h-4 flex-shrink-0" aria-hidden="true"><use href="${icons[type] || icons.info}"/></svg>
40
+ <span>${this.escapeHtml(message)}</span>
41
+ `;
42
+
43
+ this.element.appendChild(toast);
44
+
45
+ // Animate in
46
+ requestAnimationFrame(() => {
47
+ toast.classList.remove("translate-y-2", "opacity-0");
48
+ });
49
+
50
+ // Auto-dismiss after 4s
51
+ setTimeout(() => {
52
+ toast.classList.add("opacity-0", "translate-y-2");
53
+ toast.addEventListener("transitionend", () => toast.remove(), { once: true });
54
+ }, 4000);
55
+ }
56
+
57
+ escapeHtml(str) {
58
+ return String(str)
59
+ .replace(/&/g, "&amp;")
60
+ .replace(/</g, "&lt;")
61
+ .replace(/>/g, "&gt;");
62
+ }
63
+ }
@@ -0,0 +1,350 @@
1
+ /* Smooth scroll for the messages container */
2
+ #chat-messages {
3
+ scroll-behavior: smooth;
4
+ }
5
+
6
+ /* Typewriter cursor */
7
+ .cursor-blink {
8
+ animation: blink 1s step-end infinite;
9
+ color: #a855f7;
10
+ }
11
+
12
+ @keyframes blink {
13
+ from, to { opacity: 1 }
14
+ 50% { opacity: 0 }
15
+ }
16
+
17
+ /* Links inside message content */
18
+ .message-content a {
19
+ color: #9333ea;
20
+ text-decoration: underline;
21
+ text-decoration-color: rgba(147, 51, 234, 0.3);
22
+ text-underline-offset: 2px;
23
+ }
24
+
25
+ .dark .message-content a {
26
+ color: #c084fc;
27
+ text-decoration-color: rgba(192, 132, 252, 0.3);
28
+ }
29
+
30
+ /* ── Prism base overrides ────────────────────────────────────────────────── */
31
+ /* Layout/font only — no background here so light/dark themes can apply. */
32
+ pre[class*="language-"],
33
+ code[class*="language-"] {
34
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace !important;
35
+ font-size: 0.75rem !important;
36
+ line-height: 1.6 !important;
37
+ text-shadow: none !important;
38
+ }
39
+
40
+ pre[class*="language-"] {
41
+ border-radius: 0 !important;
42
+ margin: 0 !important;
43
+ white-space: pre-wrap !important;
44
+ word-wrap: break-word !important;
45
+ }
46
+
47
+ /* ── Dark mode: Prism token colours (GitHub dark palette) ────────────────── */
48
+ .dark pre[class*="language-"] {
49
+ background: #161b22 !important;
50
+ color: #e6edf3 !important;
51
+ }
52
+ code[class*="language-"] { color: #e6edf3; }
53
+ .dark .token.comment,
54
+ .dark .token.prolog,
55
+ .dark .token.doctype,
56
+ .dark .token.cdata { color: #8b949e; font-style: italic; }
57
+ .dark .token.punctuation { color: #c9d1d9; }
58
+ .dark .token.property,
59
+ .dark .token.tag,
60
+ .dark .token.boolean,
61
+ .dark .token.number,
62
+ .dark .token.constant,
63
+ .dark .token.symbol { color: #79c0ff; }
64
+ .dark .token.selector,
65
+ .dark .token.attr-name,
66
+ .dark .token.string,
67
+ .dark .token.char,
68
+ .dark .token.builtin,
69
+ .dark .token.inserted { color: #a5d6ff; }
70
+ .dark .token.operator,
71
+ .dark .token.url { color: #e6edf3; }
72
+ .dark .token.atrule,
73
+ .dark .token.attr-value,
74
+ .dark .token.keyword { color: #ff7b72; }
75
+ .dark .token.function,
76
+ .dark .token.class-name { color: #d2a8ff; }
77
+ .dark .token.regex,
78
+ .dark .token.important,
79
+ .dark .token.variable { color: #ffa657; }
80
+ .dark .token.deleted { color: #ffa198; }
81
+
82
+ /* ── SQL code viewer: always dark (Catppuccin Mocha) ────────────────────── */
83
+ [data-sql-block] pre[class*="language-"] {
84
+ background: #1e1e2e !important;
85
+ color: #cdd6f4 !important;
86
+ text-shadow: none !important;
87
+ }
88
+ [data-sql-block] .token.comment,
89
+ [data-sql-block] .token.prolog { color: #6c7086 !important; font-style: italic; }
90
+ [data-sql-block] .token.keyword { color: #cba6f7 !important; }
91
+ [data-sql-block] .token.string,
92
+ [data-sql-block] .token.char { color: #a6e3a1 !important; }
93
+ [data-sql-block] .token.number,
94
+ [data-sql-block] .token.boolean { color: #fab387 !important; }
95
+ [data-sql-block] .token.function { color: #89b4fa !important; }
96
+ [data-sql-block] .token.class-name { color: #f38ba8 !important; }
97
+ [data-sql-block] .token.operator { color: #89dceb !important; }
98
+ [data-sql-block] .token.punctuation { color: #9399b2 !important; }
99
+ [data-sql-block] .token.property,
100
+ [data-sql-block] .token.tag { color: #f2cdcd !important; }
101
+ [data-sql-block] .token.atrule,
102
+ [data-sql-block] .token.attr-value { color: #f5c2e7 !important; }
103
+ [data-sql-block] .token.variable { color: #cdd6f4 !important; }
104
+ [data-sql-block] .token.selector,
105
+ [data-sql-block] .token.inserted { color: #a6e3a1 !important; }
106
+ [data-sql-block] .token.deleted { color: #f38ba8 !important; }
107
+
108
+ /* Inline code (not inside pre) */
109
+ .message-content code:not(pre code) {
110
+ background: rgba(147, 51, 234, 0.08);
111
+ color: #9333ea;
112
+ padding: 0.125rem 0.375rem;
113
+ border-radius: 0.25rem;
114
+ font-family: ui-monospace, monospace;
115
+ font-size: 0.8em;
116
+ }
117
+
118
+ .dark .message-content code:not(pre code) {
119
+ background: rgba(192, 132, 252, 0.1);
120
+ color: #c084fc;
121
+ }
122
+
123
+ hr {
124
+ border: none;
125
+ border-top: 1px solid;
126
+ border-color: #e5e7eb;
127
+ margin: 0.75rem 0;
128
+ }
129
+
130
+ .dark hr {
131
+ border-color: #1f2937;
132
+ }
133
+
134
+ /* Code blocks inside user message bubbles (white text on purple bg) */
135
+ .user-message-prose code {
136
+ background: rgba(255, 255, 255, 0.18);
137
+ color: #f0e8ff;
138
+ padding: 0.1em 0.35em;
139
+ border-radius: 3px;
140
+ font-family: ui-monospace, monospace;
141
+ font-size: 0.85em;
142
+ }
143
+ .user-message-prose pre {
144
+ background: rgba(0, 0, 0, 0.35) !important;
145
+ border-radius: 6px;
146
+ margin: 0.5em 0;
147
+ padding: 0.75em 1em !important;
148
+ overflow-x: auto;
149
+ max-width: 100%;
150
+ white-space: pre-wrap !important;
151
+ word-break: break-all;
152
+ }
153
+ .user-message-prose pre code {
154
+ background: transparent;
155
+ padding: 0;
156
+ color: #e8d5ff;
157
+ font-size: 0.78rem !important;
158
+ }
159
+ .user-message-prose a {
160
+ color: #e9d5ff;
161
+ text-decoration: underline;
162
+ }
163
+ .user-message-prose p { margin: 0.25em 0; }
164
+
165
+ /* @mention highlight in message content — purple bold, no background, real link */
166
+ .glancer-mention {
167
+ display: inline;
168
+ background: none;
169
+ color: #7e22ce;
170
+ font-weight: 700;
171
+ padding: 0 0.25em;
172
+ border-radius: 0.2em;
173
+ text-decoration: none;
174
+ }
175
+ .glancer-mention:hover {
176
+ text-decoration: underline;
177
+ text-underline-offset: 2px;
178
+ }
179
+ .dark .glancer-mention {
180
+ background: none;
181
+ color: #c084fc;
182
+ }
183
+ /* Inside user purple bubble */
184
+ .user-message-prose .glancer-mention {
185
+ background: none;
186
+ color: #f0e8ff;
187
+ font-weight: 700;
188
+ }
189
+
190
+ /* SQL editor textarea — matches Prism code block styling */
191
+ .glancer-sql-editor {
192
+ background: #1e1e2e !important;
193
+ color: #cdd6f4 !important;
194
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace !important;
195
+ font-size: 0.75rem !important;
196
+ line-height: 1.6 !important;
197
+ tab-size: 2;
198
+ }
199
+
200
+ /* Code editor (contenteditable + Prism live highlighting) — always dark */
201
+ .glancer-code-editor {
202
+ background: #1e1e2e !important;
203
+ color: #cdd6f4 !important;
204
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace !important;
205
+ font-size: 0.75rem !important;
206
+ line-height: 1.6 !important;
207
+ tab-size: 2;
208
+ padding: 0.75rem;
209
+ white-space: pre-wrap;
210
+ word-break: break-all;
211
+ min-height: 80px;
212
+ outline: none;
213
+ caret-color: #cdd6f4;
214
+ }
215
+ /* Prism injects spans — preserve wrapping */
216
+ .glancer-code-editor * {
217
+ white-space: pre-wrap;
218
+ word-break: break-all;
219
+ }
220
+ /* Token colours inside the editor (same Catppuccin palette as [data-sql-block]) */
221
+ .glancer-code-editor .token.comment,
222
+ .glancer-code-editor .token.prolog { color: #6c7086; font-style: italic; }
223
+ .glancer-code-editor .token.keyword { color: #cba6f7; }
224
+ .glancer-code-editor .token.string,
225
+ .glancer-code-editor .token.char { color: #a6e3a1; }
226
+ .glancer-code-editor .token.number,
227
+ .glancer-code-editor .token.boolean { color: #fab387; }
228
+ .glancer-code-editor .token.function { color: #89b4fa; }
229
+ .glancer-code-editor .token.class-name { color: #f38ba8; }
230
+ .glancer-code-editor .token.operator { color: #89dceb; }
231
+ .glancer-code-editor .token.punctuation { color: #9399b2; }
232
+ .glancer-code-editor .token.property,
233
+ .glancer-code-editor .token.tag { color: #f2cdcd; }
234
+ .glancer-code-editor .token.variable { color: #cdd6f4; }
235
+ .glancer-code-editor .token.atrule,
236
+ .glancer-code-editor .token.attr-value { color: #f5c2e7; }
237
+ .glancer-code-editor .token.selector { color: #a6e3a1; }
238
+
239
+ /* @ mention backdrop (highlights @table tokens behind the textarea) */
240
+ .mention-backdrop {
241
+ position: absolute;
242
+ top: 0;
243
+ left: 0;
244
+ right: 0;
245
+ pointer-events: none;
246
+ overflow: hidden;
247
+ color: transparent;
248
+ white-space: pre-wrap;
249
+ word-break: break-word;
250
+ z-index: 0;
251
+ }
252
+ .mention-backdrop mark.mention-hl {
253
+ background: rgba(147, 51, 234, 0.15);
254
+ border-radius: 0.2em;
255
+ color: transparent;
256
+ }
257
+ .dark .mention-backdrop mark.mention-hl {
258
+ background: rgba(192, 132, 252, 0.2);
259
+ }
260
+
261
+ /* @ mention chip in chat form — links to schema page */
262
+ .mention-chip {
263
+ display: inline-flex;
264
+ align-items: center;
265
+ gap: 0.25rem;
266
+ background: none;
267
+ color: #7e22ce;
268
+ border: none;
269
+ border-radius: 9999px;
270
+ padding: 0.1rem 0.4rem;
271
+ font-size: 0.7rem;
272
+ font-weight: 700;
273
+ cursor: pointer;
274
+ text-decoration: none;
275
+ }
276
+ .mention-chip:hover {
277
+ text-decoration: underline;
278
+ text-underline-offset: 2px;
279
+ }
280
+ .dark .mention-chip {
281
+ background: none;
282
+ color: #c084fc;
283
+ }
284
+
285
+ /* ── Fullscreen table dialog ─────────────────────────────────────────────── */
286
+ .glancer-fullscreen-dialog {
287
+ position: fixed;
288
+ inset: 0;
289
+ width: 100vw;
290
+ max-width: 100vw;
291
+ height: 100vh;
292
+ max-height: 100vh;
293
+ padding: 0;
294
+ margin: 0;
295
+ border: none;
296
+ background: transparent;
297
+ overflow: hidden;
298
+ }
299
+ .glancer-fullscreen-dialog::backdrop {
300
+ background: rgba(0, 0, 0, 0.6);
301
+ }
302
+ .glancer-fullscreen-inner {
303
+ display: flex;
304
+ flex-direction: column;
305
+ width: 100%;
306
+ height: 100%;
307
+ background: #fff;
308
+ padding: 0;
309
+ }
310
+ .dark .glancer-fullscreen-inner {
311
+ background: #111827;
312
+ }
313
+ .glancer-fullscreen-toolbar {
314
+ display: flex;
315
+ align-items: center;
316
+ justify-content: space-between;
317
+ padding: 0.75rem 1.25rem;
318
+ border-bottom: 1px solid #e5e7eb;
319
+ background: #f9fafb;
320
+ flex-shrink: 0;
321
+ }
322
+ .dark .glancer-fullscreen-toolbar {
323
+ border-bottom-color: #374151;
324
+ background: #1f2937;
325
+ }
326
+ .glancer-fullscreen-title {
327
+ font-size: 0.8rem;
328
+ font-weight: 600;
329
+ color: #6b7280;
330
+ text-transform: uppercase;
331
+ letter-spacing: 0.06em;
332
+ }
333
+ .dark .glancer-fullscreen-title { color: #9ca3af; }
334
+ .glancer-fullscreen-close {
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ width: 2rem;
339
+ height: 2rem;
340
+ border-radius: 0.5rem;
341
+ color: #6b7280;
342
+ transition: background 0.15s, color 0.15s;
343
+ }
344
+ .glancer-fullscreen-close:hover { background: #f3f4f6; color: #111827; }
345
+ .dark .glancer-fullscreen-close:hover { background: #374151; color: #f9fafb; }
346
+ .glancer-fullscreen-body {
347
+ flex: 1;
348
+ overflow: auto;
349
+ padding: 1rem;
350
+ }
@@ -0,0 +1,6 @@
1
+ /* Legacy selectors kept for backward compat with any host-app overrides */
2
+ .message pre {
3
+ white-space: pre-wrap;
4
+ word-wrap: break-word;
5
+ word-break: break-word;
6
+ }
@@ -0,0 +1,31 @@
1
+ .message-content ul,
2
+ .message-content ol {
3
+ padding-left: 1.5rem;
4
+ margin: 1rem 0;
5
+ }
6
+
7
+ .message-content ul {
8
+ list-style-type: disc;
9
+ }
10
+
11
+ .message-content ol {
12
+ list-style-type: decimal;
13
+ }
14
+
15
+ .message-content li {
16
+ margin-bottom: 0.5rem;
17
+ line-height: 1.5;
18
+ color: #374151; /* gray-700 */
19
+ }
20
+
21
+ .dark .message-content li {
22
+ color: #d1d5db; /* gray-300 */
23
+ }
24
+
25
+ .message-content li::marker {
26
+ color: #6b7280; /* gray-500 */
27
+ }
28
+
29
+ .dark .message-content li::marker {
30
+ color: #9ca3af; /* gray-400 */
31
+ }
@@ -0,0 +1,16 @@
1
+ ::-webkit-scrollbar {
2
+ width: 8px;
3
+ }
4
+
5
+ ::-webkit-scrollbar-track {
6
+ background: #1a1a2e;
7
+ }
8
+
9
+ ::-webkit-scrollbar-thumb {
10
+ background: #4b5563;
11
+ border-radius: 4px;
12
+ }
13
+
14
+ ::-webkit-scrollbar-thumb:hover {
15
+ background: #6b7280;
16
+ }
@@ -0,0 +1,97 @@
1
+ table {
2
+ width: 100%;
3
+ border-collapse: collapse;
4
+ font-size: 0.875rem; /* Tailwind's text-sm */
5
+ }
6
+
7
+ thead {
8
+ background-color: #f9fafb; /* gray-50 */
9
+ }
10
+
11
+ thead th {
12
+ text-align: left;
13
+ padding: 0.75rem 1rem;
14
+ border-bottom: 1px solid #e5e7eb; /* gray-200 */
15
+ font-weight: 600;
16
+ color: #374151; /* gray-700 */
17
+ white-space: nowrap;
18
+ }
19
+
20
+ tbody td {
21
+ padding: 0.75rem 1rem;
22
+ border-bottom: 1px solid #e5e7eb; /* gray-200 */
23
+ color: #374151; /* gray-700 */
24
+ vertical-align: top;
25
+ white-space: nowrap;
26
+ }
27
+
28
+ tr:hover td {
29
+ background-color: #f3f4f6; /* gray-100 */
30
+ }
31
+
32
+ /* Dark mode support */
33
+ .dark thead {
34
+ background-color: #1f2937; /* gray-800 */
35
+ }
36
+
37
+ .dark thead th,
38
+ .dark tbody td {
39
+ border-color: #374151; /* gray-700 */
40
+ color: #d1d5db; /* gray-300 */
41
+ }
42
+
43
+ .dark tr:hover td {
44
+ background-color: #374151; /* gray-700 */
45
+ }
46
+ .message-content table {
47
+ width: 100%;
48
+ border-collapse: collapse;
49
+ font-size: 0.875rem;
50
+ }
51
+
52
+ .message-content th,
53
+ .message-content td {
54
+ padding: 0.5rem 0.75rem;
55
+ border: 1px solid #e5e7eb;
56
+ text-align: left;
57
+ white-space: nowrap;
58
+ }
59
+
60
+ .message-content thead {
61
+ background-color: #f9fafb;
62
+ position: sticky;
63
+ top: 0;
64
+ z-index: 1;
65
+ }
66
+
67
+ .dark .message-content th,
68
+ .dark .message-content td {
69
+ border-color: #374151;
70
+ color: #d1d5db;
71
+ }
72
+
73
+ .dark .message-content thead {
74
+ background-color: #1f2937;
75
+ }
76
+
77
+ .table-scroll-wrapper {
78
+ overflow-x: auto;
79
+ max-width: 100%;
80
+ margin: 1rem 0;
81
+ }
82
+
83
+ .table-scroll-inner {
84
+ max-height: 28rem; /* cerca de 10 linhas */
85
+ overflow-y: auto;
86
+ }
87
+
88
+ /* Scrollbar melhorada (opcional) */
89
+ .table-scroll-inner::-webkit-scrollbar {
90
+ height: 6px;
91
+ width: 6px;
92
+ }
93
+
94
+ .table-scroll-inner::-webkit-scrollbar-thumb {
95
+ background-color: rgba(100, 116, 139, 0.4); /* slate-500 */
96
+ border-radius: 3px;
97
+ }
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glancer
4
+ # :nodoc:
5
+ class ApplicationController < ::ApplicationController
6
+ before_action :configure_locale
7
+
8
+ # ActionController::Live runs each request in a new thread. Devise/Warden
9
+ # signals unauthenticated access via `throw :warden`, which Rack's middleware
10
+ # normally catches. Inside a Live thread that throw escapes as an
11
+ # UncaughtThrowError. Rescue it here so controllers that include Live (e.g.
12
+ # MessagesController) return a proper response instead of a 500.
13
+ rescue_from UncaughtThrowError do |e|
14
+ raise e unless defined?(::Warden) && e.tag == :warden
15
+
16
+ login_path = begin
17
+ main_app.new_user_session_path
18
+ rescue NoMethodError
19
+ "/"
20
+ end
21
+ request.format.html? ? redirect_to(login_path) : head(:unauthorized)
22
+ end
23
+
24
+ private
25
+
26
+ def configure_locale
27
+ lang = Glancer::Setting.get("ui_language", default: "en")
28
+ I18n.locale = lang.to_sym
29
+ rescue StandardError
30
+ I18n.locale = :en
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glancer
4
+ # :nodoc:
5
+ class ChatsController < Glancer::ApplicationController
6
+ layout "glancer/application"
7
+
8
+ def index
9
+ @chats = Glancer::Chat.order(created_at: :desc)
10
+ @chat = nil
11
+ end
12
+
13
+ def show
14
+ @chat = Glancer::Chat.find_by(id: params[:id])
15
+ return redirect_to(glancer.root_path, alert: "Chat not found") unless @chat
16
+
17
+ @chats = Glancer::Chat.order(created_at: :desc)
18
+ @messages = @chat.messages.order(:created_at)
19
+ end
20
+
21
+ def start
22
+ content = params[:content].to_s.strip
23
+ return render(json: { error: "Message required" }, status: :unprocessable_entity) if content.blank?
24
+
25
+ @chat = Glancer::Chat.create!(title: "New Chat")
26
+ @message = @chat.messages.create!(role: :user, content: content)
27
+
28
+ @response_message = @chat.messages.create!(
29
+ role: :assistant,
30
+ content: "",
31
+ code_type: "sql",
32
+ user_message: @message,
33
+ status: :processing
34
+ )
35
+
36
+ Glancer::AsyncRunner.call(@response_message.id, content)
37
+
38
+ render json: { chat_id: @chat.id }
39
+ rescue StandardError => e
40
+ Glancer::Utils::Logger.error("ChatsController#start", e.message)
41
+ render json: { error: e.message }, status: :internal_server_error
42
+ end
43
+
44
+ def destroy
45
+ Glancer::Chat.find(params[:id]).destroy!
46
+ redirect_to glancer.root_path
47
+ end
48
+ end
49
+ end