@mvriu5/payload-ai 0.5.6 → 0.5.9

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 (39) hide show
  1. package/README.md +52 -5
  2. package/dist/ai/providerOptions.d.ts +1 -14
  3. package/dist/ai/providerOptions.js +0 -23
  4. package/dist/ai/providerRuntime.d.ts +3 -2
  5. package/dist/ai/providerRuntime.js +6 -15
  6. package/dist/components/AIActionProposalList.d.ts +2 -1
  7. package/dist/components/AIActionProposalList.js +74 -6
  8. package/dist/components/AIActionProposalList.module.css +49 -5
  9. package/dist/components/AIApiKeyField.js +0 -8
  10. package/dist/components/AIInput.d.ts +1 -1
  11. package/dist/components/AIInput.js +156 -23
  12. package/dist/components/AIInput.module.css +185 -147
  13. package/dist/components/CollectionMentionPopover.d.ts +1 -1
  14. package/dist/components/DiffDialog.d.ts +12 -0
  15. package/dist/components/DiffDialog.js +310 -0
  16. package/dist/components/DiffDialog.module.css +249 -0
  17. package/dist/components/Icons.d.ts +9 -0
  18. package/dist/components/Icons.js +266 -0
  19. package/dist/components/RecentChangesList.d.ts +18 -0
  20. package/dist/components/RecentChangesList.js +66 -0
  21. package/dist/components/RecentChangesList.module.css +135 -0
  22. package/dist/components/hooks/useAISettings.d.ts +1 -1
  23. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +3 -1
  24. package/dist/endpoints/aiApplyActionEndpointHandler.js +209 -8
  25. package/dist/endpoints/aiChatEndpointHandler.d.ts +5 -1
  26. package/dist/endpoints/aiChatEndpointHandler.js +106 -21
  27. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +2 -1
  28. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +7 -7
  29. package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +7 -0
  30. package/dist/endpoints/aiProposalDiffEndpointHandler.js +138 -0
  31. package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +6 -0
  32. package/dist/endpoints/aiRecentChangesEndpointHandler.js +35 -0
  33. package/dist/index.d.ts +3 -1
  34. package/dist/index.js +140 -18
  35. package/dist/payload/collectionPermissions.d.ts +22 -0
  36. package/dist/payload/collectionPermissions.js +68 -0
  37. package/dist/payload/schemaContext.d.ts +46 -2
  38. package/dist/payload/schemaContext.js +105 -14
  39. package/package.json +113 -121
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
  import { useConfig } from "@payloadcms/ui";
3
3
  import { formatAdminURL } from "payload/shared";
4
- import { useMemo, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { getResolvedAIModelConfig } from "../ai/providerOptions.js";
6
6
  import { getSerializableLabel, isInternalCollection } from "../payload/shared.js";
7
7
  import { AIActionProposalList } from "./AIActionProposalList.js";
8
8
  import styles from "./AIInput.module.css";
9
9
  import { CollectionMentionPopover } from "./CollectionMentionPopover.js";
10
+ import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon, Send } from "./Icons.js";
11
+ import { RecentChangesList } from "./RecentChangesList.js";
10
12
  import { useAISettings } from "./hooks/useAISettings.js";
11
13
  import { useDocumentMentionSuggestions } from "./hooks/useDocumentMentionSuggestions.js";
12
14
  const collectBlockOptions = ({ fields, parent })=>{
@@ -35,6 +37,52 @@ const collectBlockOptions = ({ fields, parent })=>{
35
37
  }
36
38
  return options;
37
39
  };
40
+ const getProviderIcon = (provider)=>{
41
+ const iconProps = {
42
+ "aria-hidden": true,
43
+ className: styles.selectProviderIcon
44
+ };
45
+ switch(provider){
46
+ case "claude":
47
+ return /*#__PURE__*/ React.createElement(ClaudeIcon, iconProps);
48
+ case "google":
49
+ return /*#__PURE__*/ React.createElement(GoogleGeminiIcon, iconProps);
50
+ case "mistral":
51
+ return /*#__PURE__*/ React.createElement(MistralAiIcon, iconProps);
52
+ case "openai":
53
+ return /*#__PURE__*/ React.createElement(OpenaiIcon, iconProps);
54
+ default:
55
+ return null;
56
+ }
57
+ };
58
+ const parseSSEEvent = (chunk)=>{
59
+ const lines = chunk.split("\n");
60
+ let eventName = "";
61
+ const dataLines = [];
62
+ for (const line of lines){
63
+ if (line.startsWith("event:")) {
64
+ eventName = line.slice(6).trim();
65
+ continue;
66
+ }
67
+ if (line.startsWith("data:")) {
68
+ dataLines.push(line.slice(5).trim());
69
+ }
70
+ }
71
+ if (!eventName) return null;
72
+ try {
73
+ const data = JSON.parse(dataLines.join("\n"));
74
+ if (eventName !== "text" && eventName !== "proposals" && eventName !== "error" && eventName !== "done") {
75
+ return null;
76
+ }
77
+ return {
78
+ data,
79
+ event: eventName
80
+ };
81
+ } catch {
82
+ return null;
83
+ }
84
+ };
85
+ const sanitizeResponseText = (value)=>value.replace(/\*\*/g, "");
38
86
  export const AIInput = ()=>{
39
87
  const { config } = useConfig();
40
88
  const editorRef = useRef(null);
@@ -56,9 +104,48 @@ export const AIInput = ()=>{
56
104
  const [response, setResponse] = useState("");
57
105
  const [error, setError] = useState("");
58
106
  const [proposals, setProposals] = useState([]);
107
+ const [appliedChanges, setAppliedChanges] = useState([]);
59
108
  const [isLoading, setIsLoading] = useState(false);
60
109
  const [isApplying, setIsApplying] = useState(false);
61
- const collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).map((collection)=>({
110
+ const enabledCollectionSlugs = config.admin?.custom?.payloadAiPlugin?.collectionSlugs;
111
+ const enabledCollectionSlugSet = useMemo(()=>enabledCollectionSlugs ? new Set(enabledCollectionSlugs) : null, [
112
+ enabledCollectionSlugs
113
+ ]);
114
+ const isCollectionMentionEnabled = (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug);
115
+ const recentChangesEndpoint = useMemo(()=>formatAdminURL({
116
+ apiRoute: config.routes.api,
117
+ path: "/ai-recent-changes"
118
+ }), [
119
+ config.routes.api
120
+ ]);
121
+ const loadRecentChanges = useCallback(async ()=>{
122
+ const res = await fetch(recentChangesEndpoint);
123
+ const result = await res.json().catch(()=>null);
124
+ if (res.ok && result?.changes) {
125
+ setAppliedChanges(result.changes);
126
+ }
127
+ }, [
128
+ recentChangesEndpoint
129
+ ]);
130
+ useEffect(()=>{
131
+ void loadRecentChanges().catch(()=>undefined);
132
+ }, [
133
+ loadRecentChanges
134
+ ]);
135
+ useEffect(()=>{
136
+ if (isLoading || error || proposals.length > 0 || !response) return;
137
+ const timeout = window.setTimeout(()=>{
138
+ setResponse("");
139
+ clearInput();
140
+ }, 5000);
141
+ return ()=>window.clearTimeout(timeout);
142
+ }, [
143
+ error,
144
+ isLoading,
145
+ proposals.length,
146
+ response
147
+ ]);
148
+ const collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).filter((collection)=>isCollectionMentionEnabled(collection.slug)).map((collection)=>({
62
149
  label: getSerializableLabel(collection.labels?.singular, collection.slug),
63
150
  slug: collection.slug,
64
151
  type: "collection"
@@ -69,7 +156,7 @@ export const AIInput = ()=>{
69
156
  type: "global"
70
157
  })) || [];
71
158
  const blocks = [
72
- ...config.collections.flatMap((collection)=>collectBlockOptions({
159
+ ...config.collections.filter((collection)=>isCollectionMentionEnabled(collection.slug)).flatMap((collection)=>collectBlockOptions({
73
160
  fields: collection.fields,
74
161
  parent: collection.slug
75
162
  })),
@@ -106,14 +193,6 @@ export const AIInput = ()=>{
106
193
  clonedRange.setEnd(range.endContainer, range.endOffset);
107
194
  return clonedRange.toString().length;
108
195
  };
109
- const moveCaretToEnd = (element)=>{
110
- const selection = window.getSelection();
111
- const range = document.createRange();
112
- range.selectNodeContents(element);
113
- range.collapse(false);
114
- selection?.removeAllRanges();
115
- selection?.addRange(range);
116
- };
117
196
  const getTextNodeAtOffset = (element, offset)=>{
118
197
  const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
119
198
  let currentOffset = 0;
@@ -231,6 +310,9 @@ export const AIInput = ()=>{
231
310
  if (!trimmedPrompt) return;
232
311
  setIsLoading(true);
233
312
  setAppliedProposalIndexes([]);
313
+ setError("");
314
+ setProposals([]);
315
+ setResponse("");
234
316
  try {
235
317
  const res = await fetch(formatAdminURL({
236
318
  apiRoute: config.routes.api,
@@ -246,15 +328,49 @@ export const AIInput = ()=>{
246
328
  },
247
329
  method: "POST"
248
330
  });
249
- const result = await res.json();
250
331
  if (!res.ok) {
251
- setProposals([]);
252
- setResponse("");
253
- throw new Error(result.error || "AI request failed");
332
+ const result = await res.json().catch(()=>null);
333
+ throw new Error(result?.error || "AI request failed");
334
+ }
335
+ if (!res.body) {
336
+ throw new Error("AI response stream is unavailable");
337
+ }
338
+ const reader = res.body.getReader();
339
+ const decoder = new TextDecoder();
340
+ let buffer = "";
341
+ while(true){
342
+ const { done, value } = await reader.read();
343
+ if (done) break;
344
+ buffer += decoder.decode(value, {
345
+ stream: true
346
+ });
347
+ const chunks = buffer.split("\n\n");
348
+ buffer = chunks.pop() || "";
349
+ for (const chunk of chunks){
350
+ const event = parseSSEEvent(chunk);
351
+ if (!event) continue;
352
+ if (event.event === "text") {
353
+ if (event.data.delta) {
354
+ setResponse((current)=>current + sanitizeResponseText(event.data.delta || ""));
355
+ }
356
+ continue;
357
+ }
358
+ if (event.event === "proposals") {
359
+ setProposals(event.data.proposals || []);
360
+ continue;
361
+ }
362
+ if (event.event === "error") {
363
+ throw new Error(event.data.error || "AI request failed");
364
+ }
365
+ }
366
+ }
367
+ const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
368
+ if (finalEvent?.event === "proposals") {
369
+ setProposals(finalEvent.data.proposals || []);
370
+ }
371
+ if (finalEvent?.event === "error") {
372
+ throw new Error(finalEvent.data.error || "AI request failed");
254
373
  }
255
- setError("");
256
- setResponse(result.text || "");
257
- setProposals(result.proposals || []);
258
374
  } catch (err) {
259
375
  setProposals([]);
260
376
  setResponse("");
@@ -289,6 +405,13 @@ export const AIInput = ()=>{
289
405
  setError("");
290
406
  setProposals([]);
291
407
  setResponse("");
408
+ if (result.change) {
409
+ setAppliedChanges((current)=>[
410
+ result.change,
411
+ ...current
412
+ ].slice(0, 12));
413
+ }
414
+ void loadRecentChanges().catch(()=>undefined);
292
415
  clearInput();
293
416
  } catch (err) {
294
417
  setError(err instanceof Error ? err.message : "Could not apply proposal");
@@ -297,6 +420,8 @@ export const AIInput = ()=>{
297
420
  }
298
421
  };
299
422
  return /*#__PURE__*/ React.createElement("div", {
423
+ className: styles.chatLayout
424
+ }, /*#__PURE__*/ React.createElement("div", {
300
425
  className: styles.chat
301
426
  }, /*#__PURE__*/ React.createElement("div", {
302
427
  className: styles.chatHeader
@@ -349,22 +474,28 @@ export const AIInput = ()=>{
349
474
  className: styles.setting
350
475
  }, /*#__PURE__*/ React.createElement("span", {
351
476
  className: styles.settingLabel
352
- }, "Model"), /*#__PURE__*/ React.createElement("select", {
477
+ }, "Model"), /*#__PURE__*/ React.createElement("div", {
478
+ className: styles.selectWrapper
479
+ }, getProviderIcon(settingsProvider), /*#__PURE__*/ React.createElement("select", {
353
480
  className: styles.select,
354
481
  disabled: !settingsProvider,
355
482
  onChange: (event)=>setSelectedModel(event.target.value),
356
483
  value: selectedModel
357
- }, !settingsProvider ? /*#__PURE__*/ React.createElement("option", {
484
+ }, !settingsProvider && /*#__PURE__*/ React.createElement("option", {
358
485
  value: ""
359
- }, "Select provider in account settings") : null, settingsProvider ? aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ React.createElement("option", {
486
+ }, "Select provider in account settings"), settingsProvider && aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ React.createElement("option", {
360
487
  key: model.value,
361
488
  value: model.value
362
- }, model.label)) : null))), /*#__PURE__*/ React.createElement("button", {
489
+ }, model.label)))))), /*#__PURE__*/ React.createElement("button", {
363
490
  className: styles.chatButton,
364
491
  disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading,
365
492
  onClick: ()=>void handleSubmit(),
366
493
  type: "button"
367
- }, isLoading ? "Sending..." : "Send")), /*#__PURE__*/ React.createElement(AIActionProposalList, {
494
+ }, /*#__PURE__*/ React.createElement(Send, {
495
+ width: 14,
496
+ height: 14
497
+ }), isLoading ? "Sending..." : "Send")), /*#__PURE__*/ React.createElement(AIActionProposalList, {
498
+ apiRoute: config.routes.api,
368
499
  appliedProposalIndexes: appliedProposalIndexes,
369
500
  description: response,
370
501
  error: error,
@@ -382,5 +513,7 @@ export const AIInput = ()=>{
382
513
  },
383
514
  onApply: (proposal, _index)=>void handleApplyProposal(proposal),
384
515
  proposals: proposals
516
+ })), /*#__PURE__*/ React.createElement(RecentChangesList, {
517
+ changes: appliedChanges
385
518
  }));
386
519
  };
@@ -1,237 +1,275 @@
1
+ .chatLayout {
2
+ align-items: stretch;
3
+ display: grid;
4
+ gap: 16px;
5
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
6
+ margin-bottom: 24px;
7
+ max-width: 1280px;
8
+ }
9
+
1
10
  .chat {
2
- border: 1px solid var(--theme-elevation-150);
3
- border-radius: 8px;
4
- margin-bottom: 24px;
5
- max-width: 920px;
6
- padding: 20px;
11
+ border: 1px solid var(--theme-elevation-150);
12
+ border-radius: 8px;
13
+ padding: 20px;
7
14
  }
8
15
 
9
16
  .chatHeader {
10
- align-items: flex-start;
11
- display: flex;
12
- justify-content: space-between;
13
- margin-bottom: 16px;
17
+ align-items: flex-start;
18
+ display: flex;
19
+ justify-content: space-between;
20
+ margin-bottom: 16px;
14
21
  }
15
22
 
16
23
  .chatTitle {
17
- font-size: 18px;
18
- line-height: 1.3;
19
- margin: 0;
24
+ font-size: 18px;
25
+ line-height: 1.3;
26
+ margin: 0;
20
27
  }
21
28
 
22
29
  .chatDescription {
23
- color: var(--theme-elevation-600);
24
- font-size: 13px;
25
- line-height: 1.4;
26
- margin: 4px 0 0;
30
+ color: var(--theme-elevation-600);
31
+ font-size: 13px;
32
+ line-height: 1.4;
33
+ margin: 4px 0 0;
27
34
  }
28
35
 
29
36
  .settings {
30
- align-items: flex-end;
31
- display: flex;
32
- gap: 10px;
37
+ align-items: flex-end;
38
+ display: flex;
39
+ gap: 10px;
33
40
  }
34
41
 
35
42
  .setting {
36
- display: flex;
37
- flex-direction: column;
38
- gap: 4px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 4px;
39
46
  }
40
47
 
41
48
  .settingLabel {
42
- color: var(--theme-elevation-600);
43
- font-size: 11px;
44
- line-height: 1.2;
49
+ color: var(--theme-elevation-600);
50
+ font-size: 11px;
51
+ line-height: 1.2;
52
+ }
53
+
54
+ .selectWrapper {
55
+ align-items: center;
56
+ display: flex;
57
+ position: relative;
58
+ }
59
+
60
+ .selectProviderIcon {
61
+ color: var(--theme-text);
62
+ height: 12px;
63
+ left: 10px;
64
+ pointer-events: none;
65
+ position: absolute;
66
+ top: 50%;
67
+ transform: translateY(-50%);
68
+ width: 16px;
45
69
  }
46
70
 
47
71
  .select {
48
- background: var(--theme-input-bg);
49
- border: 1px solid var(--theme-elevation-150);
50
- border-radius: 4px;
51
- color: var(--theme-text);
52
- font: inherit;
53
- min-height: 34px;
54
- min-width: 160px;
55
- padding: 0 28px 0 10px;
72
+ background: var(--theme-input-bg);
73
+ border: 1px solid var(--theme-elevation-150);
74
+ border-radius: 4px;
75
+ color: var(--theme-text);
76
+ font: inherit;
77
+ min-height: 24px;
78
+ min-width: 160px;
79
+ padding: 0 28px 0 34px;
56
80
  }
57
81
 
58
82
  .select:focus {
59
- border-color: var(--theme-success-500);
60
- outline: none;
83
+ border-color: var(--theme-success-500);
84
+ outline: none;
61
85
  }
62
86
 
63
87
  .chatInputRow {
64
- position: relative;
65
- width: 100%;
88
+ position: relative;
89
+ width: 100%;
66
90
  }
67
91
 
68
92
  .chatActionsRow {
69
- align-items: flex-end;
70
- display: flex;
71
- gap: 12px;
72
- justify-content: space-between;
73
- margin-top: 12px;
93
+ align-items: flex-end;
94
+ display: flex;
95
+ gap: 12px;
96
+ justify-content: space-between;
97
+ margin-top: 12px;
74
98
  }
75
99
 
76
100
  .chatInput {
77
- background: transparent;
78
- border: 0;
79
- color: var(--theme-text);
80
- font: inherit;
81
- min-height: 84px;
82
- padding: 0;
83
- white-space: pre-wrap;
84
- width: 100%;
101
+ background: transparent;
102
+ border: 0;
103
+ color: var(--theme-text);
104
+ font: inherit;
105
+ min-height: 84px;
106
+ padding: 0;
107
+ white-space: pre-wrap;
108
+ width: 100%;
85
109
  }
86
110
 
87
111
  .chatInput:focus {
88
- outline: none;
112
+ outline: none;
89
113
  }
90
114
 
91
115
  .chatInput:empty::before {
92
- color: var(--theme-elevation-400);
93
- content: attr(data-placeholder);
116
+ color: var(--theme-elevation-400);
117
+ content: attr(data-placeholder);
94
118
  }
95
119
 
96
120
  .chatInputSurface {
97
- background: var(--theme-input-bg);
98
- border: 1px solid var(--theme-elevation-150);
99
- border-radius: 4px;
100
- padding: 12px;
101
- width: 100%;
121
+ background: var(--theme-input-bg);
122
+ border: 1px solid var(--theme-elevation-150);
123
+ border-radius: 4px;
124
+ padding: 12px;
125
+ width: 100%;
102
126
  }
103
127
 
104
128
  .chatInputSurface:focus-within {
105
- border-color: var(--theme-success-500);
129
+ border-color: var(--theme-success-500);
106
130
  }
107
131
 
108
132
  .inlineBadge {
109
- margin: 0 2px;
110
- vertical-align: middle;
133
+ margin: 0 2px;
134
+ vertical-align: middle;
111
135
  }
112
136
 
113
137
  .badge {
114
- align-items: center;
115
- background: var(--theme-elevation-100);
116
- border: 1px solid var(--theme-elevation-150);
117
- border-radius: 999px;
118
- color: var(--theme-text);
119
- display: inline-flex;
120
- font-size: 11px;
121
- gap: 3px;
122
- line-height: 1;
123
- min-height: 20px;
124
- padding: 0 8px;
125
- position: relative;
126
- top: -2px;
127
- white-space: nowrap;
138
+ align-items: center;
139
+ background: var(--theme-elevation-100);
140
+ border: 1px solid var(--theme-elevation-150);
141
+ border-radius: 999px;
142
+ color: var(--theme-text);
143
+ display: inline-flex;
144
+ font-size: 11px;
145
+ gap: 3px;
146
+ line-height: 1;
147
+ min-height: 20px;
148
+ padding: 0 8px;
149
+ position: relative;
150
+ top: -2px;
151
+ white-space: nowrap;
128
152
  }
129
153
 
130
154
  .prefix {
131
- color: var(--theme-elevation-500);
155
+ color: var(--theme-elevation-500);
132
156
  }
133
157
 
134
158
  .name {
135
- color: var(--theme-text);
159
+ color: var(--theme-text);
136
160
  }
137
161
 
138
162
  .collection {
139
- background: color-mix(in srgb, #2f7d68 12%, var(--theme-bg));
140
- border-color: color-mix(in srgb, #2f7d68 28%, var(--theme-elevation-150));
163
+ background: color-mix(in srgb, #2f7d68 12%, var(--theme-bg));
164
+ border-color: color-mix(in srgb, #2f7d68 28%, var(--theme-elevation-150));
141
165
  }
142
166
 
143
167
  .global {
144
- background: color-mix(in srgb, #8b6f24 12%, var(--theme-bg));
145
- border-color: color-mix(in srgb, #8b6f24 28%, var(--theme-elevation-150));
168
+ background: color-mix(in srgb, #8b6f24 12%, var(--theme-bg));
169
+ border-color: color-mix(in srgb, #8b6f24 28%, var(--theme-elevation-150));
146
170
  }
147
171
 
148
172
  .block {
149
- background: color-mix(in srgb, #5a6fa8 12%, var(--theme-bg));
150
- border-color: color-mix(in srgb, #5a6fa8 28%, var(--theme-elevation-150));
173
+ background: color-mix(in srgb, #5a6fa8 12%, var(--theme-bg));
174
+ border-color: color-mix(in srgb, #5a6fa8 28%, var(--theme-elevation-150));
151
175
  }
152
176
 
153
177
  .doc {
154
- background: color-mix(in srgb, #7a5c9e 12%, var(--theme-bg));
155
- border-color: color-mix(in srgb, #7a5c9e 28%, var(--theme-elevation-150));
178
+ background: color-mix(in srgb, #7a5c9e 12%, var(--theme-bg));
179
+ border-color: color-mix(in srgb, #7a5c9e 28%, var(--theme-elevation-150));
156
180
  }
157
181
 
158
182
  .chatButton {
159
- background: var(--theme-text);
160
- border: 0;
161
- border-radius: 4px;
162
- color: var(--theme-bg);
163
- cursor: pointer;
164
- font: inherit;
165
- min-height: 40px;
166
- padding: 0 18px;
183
+ align-items: center;
184
+ background: var(--theme-text);
185
+ border: 0;
186
+ border-radius: 4px;
187
+ color: var(--theme-bg);
188
+ cursor: pointer;
189
+ display: inline-flex;
190
+ font: inherit;
191
+ gap: 6px;
192
+ justify-content: center;
193
+ line-height: 24px;
194
+ min-height: 24px;
195
+ padding: 0 14px;
196
+ }
197
+
198
+ .chatButton svg {
199
+ flex: 0 0 auto;
167
200
  }
168
201
 
169
202
  .chatButton:disabled {
170
- cursor: not-allowed;
171
- opacity: 0.45;
203
+ cursor: not-allowed;
204
+ opacity: 0.45;
172
205
  }
173
206
 
174
207
  .chatError {
175
- color: var(--theme-error-500);
176
- font-size: 13px;
177
- line-height: 1.4;
178
- margin-top: 12px;
208
+ color: var(--theme-error-500);
209
+ font-size: 13px;
210
+ line-height: 1.4;
211
+ margin-top: 12px;
179
212
  }
180
213
 
181
214
  .debugInfo {
182
- background: var(--theme-elevation-50);
183
- border: 1px solid var(--theme-elevation-150);
184
- border-radius: 4px;
185
- color: var(--theme-elevation-800);
186
- font-size: 12px;
187
- line-height: 1.5;
188
- margin: 12px 0 0;
189
- max-height: 260px;
190
- overflow: auto;
191
- padding: 12px;
192
- white-space: pre-wrap;
215
+ background: var(--theme-elevation-50);
216
+ border: 1px solid var(--theme-elevation-150);
217
+ border-radius: 4px;
218
+ color: var(--theme-elevation-800);
219
+ font-size: 12px;
220
+ line-height: 1.5;
221
+ margin: 12px 0 0;
222
+ max-height: 260px;
223
+ overflow: auto;
224
+ padding: 12px;
225
+ white-space: pre-wrap;
193
226
  }
194
227
 
195
228
  .chatResponse {
196
- background: var(--theme-elevation-50);
197
- border: 1px solid var(--theme-elevation-100);
198
- border-radius: 4px;
199
- line-height: 1.5;
200
- margin-top: 16px;
201
- padding: 12px;
202
- white-space: pre-wrap;
229
+ background: var(--theme-elevation-50);
230
+ border: 1px solid var(--theme-elevation-100);
231
+ border-radius: 4px;
232
+ line-height: 1.5;
233
+ margin-top: 16px;
234
+ padding: 12px;
235
+ white-space: pre-wrap;
203
236
  }
204
237
 
205
238
  @media (max-width: 768px) {
206
- .chatHeader {
207
- flex-direction: column;
208
- gap: 12px;
209
- }
210
-
211
- .chatInputRow {
212
- width: 100%;
213
- }
214
-
215
- .chatActionsRow {
216
- align-items: stretch;
217
- flex-direction: column;
218
- }
219
-
220
- .settings {
221
- align-items: stretch;
222
- width: 100%;
223
- }
224
-
225
- .setting {
226
- flex: 1;
227
- }
228
-
229
- .select {
230
- min-width: 0;
231
- width: 100%;
232
- }
239
+ .chatLayout {
240
+ grid-template-columns: minmax(0, 1fr);
241
+ }
242
+
243
+ .chatHeader {
244
+ flex-direction: column;
245
+ gap: 12px;
246
+ }
247
+
248
+ .chatInputRow {
249
+ width: 100%;
250
+ }
251
+
252
+ .chatActionsRow {
253
+ align-items: stretch;
254
+ flex-direction: column;
255
+ }
256
+
257
+ .settings {
258
+ align-items: stretch;
259
+ width: 100%;
260
+ }
261
+
262
+ .setting {
263
+ flex: 1;
264
+ }
265
+
266
+ .select {
267
+ min-width: 0;
268
+ width: 100%;
269
+ }
270
+
271
+ .chatButton {
272
+ align-self: stretch;
273
+ }
233
274
 
234
- .chatButton {
235
- align-self: stretch;
236
- }
237
275
  }