@things-factory/ai-assistant 10.1.109 → 10.1.111

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.
@@ -29,6 +29,7 @@ import gql from 'graphql-tag';
29
29
  import { renderMarkdown, injectMentionRefids, injectMentionUserIds, stripMentionRefids } from './markdown.js';
30
30
  import { acceptOf, chatAttachmentUrl, refuseAttachments, textCharsOf } from '../utils/attachment-policy.js';
31
31
  import { pastedTableFile } from '../utils/pasted-table.js';
32
+ import { appendWords, composerHeight, composerKeyAction, sendShortcut, tableAsWords } from '../utils/composer-frame.js';
32
33
  import { readSpreadsheet, spreadsheetKindOf } from '../utils/spreadsheet-text.js';
33
34
  import { entryReviewAddress, offersWorkspaceReview } from '../utils/entry-workspace.js';
34
35
  import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder.js';
@@ -295,6 +296,10 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
295
296
  this.attachments = [];
296
297
  /** 드래그 오버 상태 (시각 피드백용) */
297
298
  this.dragOver = false;
299
+ /** The wide box (넓게 쓰기) covers the conversation inside the dock - shared-dock.md §10. */
300
+ this.composerWide = false;
301
+ /** The pasted table whose rows cover the conversation, or null. */
302
+ this.previewAttachmentId = null;
298
303
  /** mini action — 인라인 예시 토글 */
299
304
  this.examplesOpen = false;
300
305
  /** 원본 JSON 을 펼친 단계(라인:단계) — 목록은 한 줄 요약으로 훑고 필요한 것만 펼친다. */
@@ -449,7 +454,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
449
454
  params.kinds = refusal.kinds.join(', ');
450
455
  return String(i18next.t(`ai-assistant.text.${refusal.code}`, { ...params, defaultValue: refusal.code }));
451
456
  }
452
- async onAttachFiles(files) {
457
+ async onAttachFiles(files, table) {
453
458
  if (!files || files.length === 0)
454
459
  return;
455
460
  let list = Array.from(files);
@@ -488,7 +493,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
488
493
  size: file.size,
489
494
  type: file.type || 'application/octet-stream',
490
495
  url,
491
- base64
496
+ base64,
497
+ table
492
498
  });
493
499
  }
494
500
  this.attachments = [...this.attachments, ...newAttachments];
@@ -531,6 +537,40 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
531
537
  URL.revokeObjectURL(target.url);
532
538
  }
533
539
  this.attachments = this.attachments.filter(a => a.id !== id);
540
+ if (this.previewAttachmentId === id)
541
+ this.previewAttachmentId = null;
542
+ }
543
+ /** A pasted table goes back into the box as the words it was copied as - the conversion was not the person's choice. */
544
+ tableToWords(id) {
545
+ const target = this.attachments.find(a => a.id === id);
546
+ if (!target?.table)
547
+ return;
548
+ this.input = appendWords(this.input, tableAsWords(target.table));
549
+ this.removeAttachment(id);
550
+ this.updateComplete.then(() => this.renderRoot.querySelector('textarea')?.focus());
551
+ }
552
+ /** The small box stands as tall as its text, up to 40% of the chat; the wide box fills what it covers. */
553
+ fitComposer() {
554
+ const ta = this.renderRoot.querySelector('.input-row textarea');
555
+ if (!ta)
556
+ return;
557
+ if (this.composerWide) {
558
+ ta.style.height = '';
559
+ ta.style.overflowY = '';
560
+ return;
561
+ }
562
+ ta.style.height = '0px';
563
+ const border = ta.offsetHeight - ta.clientHeight;
564
+ const resting = parseFloat(getComputedStyle(ta).minHeight) || 0;
565
+ const { height, scrolls } = composerHeight(ta.scrollHeight + border, this.clientHeight, resting);
566
+ ta.style.height = `${height}px`;
567
+ ta.style.overflowY = scrolls ? 'auto' : 'hidden';
568
+ }
569
+ setComposerWide(wide) {
570
+ this.composerWide = wide;
571
+ if (wide)
572
+ this.previewAttachmentId = null;
573
+ this.updateComplete.then(() => this.renderRoot.querySelector('.input-row textarea')?.focus());
534
574
  }
535
575
  onComposerPaste(e) {
536
576
  const items = e.clipboardData?.items;
@@ -548,7 +588,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
548
588
  this.errorMessage = String(i18next.t(`ai-assistant.text.${pasted.refusal.code}`, { ...pasted.refusal, defaultValue: pasted.refusal.code }));
549
589
  return;
550
590
  }
551
- this.onAttachFiles([new File([pasted.text], pasted.name, { type: 'text/csv' })]);
591
+ const words = e.clipboardData?.getData('text/plain');
592
+ this.onAttachFiles([new File([pasted.text], pasted.name, { type: 'text/csv' })], { ...pasted.table, text: words });
552
593
  return;
553
594
  }
554
595
  }
@@ -2015,6 +2056,128 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2015
2056
  flex-direction: column;
2016
2057
  transition: background 0.15s, border-color 0.15s;
2017
2058
  }
2059
+ /* The wide box and a table's rows cover the conversation, inside the dock - the page beside it stays. */
2060
+ .messages.covered {
2061
+ flex: 0 0 0;
2062
+ padding: 0;
2063
+ overflow: hidden;
2064
+ visibility: hidden;
2065
+ }
2066
+ .composer.wide,
2067
+ .composer.previewing {
2068
+ flex: 1 1 auto;
2069
+ min-height: 0;
2070
+ }
2071
+ .composer.wide .input-row {
2072
+ flex: 1;
2073
+ min-height: 0;
2074
+ align-items: stretch;
2075
+ }
2076
+ .composer.wide .input-row > :not(.textarea-wrap) {
2077
+ align-self: flex-end;
2078
+ }
2079
+ .composer.wide textarea {
2080
+ min-height: 0;
2081
+ }
2082
+ .composer-size {
2083
+ margin-left: auto;
2084
+ }
2085
+ /* Named above the send button, out of the row - it takes no width from the box. */
2086
+ .composer.wide .input-row {
2087
+ position: relative;
2088
+ }
2089
+ .send-shortcut {
2090
+ position: absolute;
2091
+ right: 14px;
2092
+ bottom: 48px;
2093
+ font-size: 10px;
2094
+ color: var(--md-sys-color-outline, #64748b);
2095
+ white-space: nowrap;
2096
+ pointer-events: none;
2097
+ }
2098
+ /* A finger has no Ctrl or ⌘ - the send button is the way. */
2099
+ @media (pointer: coarse) {
2100
+ .send-shortcut {
2101
+ display: none;
2102
+ }
2103
+ }
2104
+ .table-preview {
2105
+ flex: 1;
2106
+ min-height: 0;
2107
+ display: flex;
2108
+ flex-direction: column;
2109
+ margin: 4px 14px;
2110
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2111
+ border-radius: 8px;
2112
+ background: var(--md-sys-color-surface, #ffffff);
2113
+ overflow: hidden;
2114
+ }
2115
+ .table-preview-head {
2116
+ display: flex;
2117
+ align-items: center;
2118
+ gap: 6px;
2119
+ padding: 6px 8px;
2120
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2121
+ flex-wrap: wrap;
2122
+ }
2123
+ .table-preview-head > md-icon {
2124
+ --md-icon-size: 18px;
2125
+ color: var(--md-sys-color-primary, #0284c7);
2126
+ }
2127
+ .table-preview-name {
2128
+ font-weight: 500;
2129
+ color: var(--md-sys-color-on-surface, #0f172a);
2130
+ min-width: 0;
2131
+ overflow: hidden;
2132
+ text-overflow: ellipsis;
2133
+ white-space: nowrap;
2134
+ }
2135
+ .table-preview-shape {
2136
+ color: var(--md-sys-color-on-surface-variant, #475569);
2137
+ margin-right: auto;
2138
+ }
2139
+ .table-preview-body {
2140
+ flex: 1;
2141
+ min-height: 0;
2142
+ overflow: auto;
2143
+ }
2144
+ .table-preview-body table {
2145
+ border-collapse: collapse;
2146
+ font-size: 11px;
2147
+ }
2148
+ .table-preview-body th,
2149
+ .table-preview-body td {
2150
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2151
+ padding: 4px 8px;
2152
+ text-align: left;
2153
+ white-space: nowrap;
2154
+ color: var(--md-sys-color-on-surface, #0f172a);
2155
+ }
2156
+ .table-preview-body th {
2157
+ position: sticky;
2158
+ left: 0;
2159
+ font-weight: 400;
2160
+ text-align: right;
2161
+ color: var(--md-sys-color-on-surface-variant, #475569);
2162
+ background: var(--md-sys-color-surface-container, #f1f5f9);
2163
+ }
2164
+ .attachment-open {
2165
+ all: unset;
2166
+ display: flex;
2167
+ align-items: center;
2168
+ gap: 6px;
2169
+ min-width: 0;
2170
+ flex: 1;
2171
+ cursor: pointer;
2172
+ border-radius: 6px;
2173
+ }
2174
+ .attachment-open:focus-visible {
2175
+ outline: 2px solid var(--md-sys-color-primary, #0284c7);
2176
+ outline-offset: 2px;
2177
+ }
2178
+ .attachment-chip.open {
2179
+ border-color: var(--md-sys-color-primary, #0284c7);
2180
+ }
2018
2181
  .composer.drag-over {
2019
2182
  background: var(--md-sys-color-surface-container, #f1f5f9);
2020
2183
  border-top-color: var(--md-sys-color-primary, #0284c7);
@@ -2281,8 +2444,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2281
2444
  font: inherit;
2282
2445
  letter-spacing: inherit;
2283
2446
  resize: none;
2284
- min-height: 36px;
2285
- max-height: 140px;
2447
+ /* The resting height of one line as it stood; the box grows from here by composerHeight (shared-dock.md §10). */
2448
+ box-sizing: border-box;
2449
+ min-height: 54px;
2286
2450
  outline: none;
2287
2451
  transition:
2288
2452
  border-color 0.2s,
@@ -2386,6 +2550,15 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2386
2550
  this.loadModeView();
2387
2551
  if (this.subject)
2388
2552
  this.onSubjectChanged();
2553
+ /*
2554
+ * Put back after the dock was closed: what is being written stays, the conversation is read again and its
2555
+ * broadcast and presence start again - they were stopped when it was taken out.
2556
+ */
2557
+ if (this.hasUpdated && this.sessionId) {
2558
+ this.loadHistory();
2559
+ this._startPresence();
2560
+ this._startMessageSubscription();
2561
+ }
2389
2562
  }
2390
2563
  /**
2391
2564
  * 화면이 보여 주는 것의 종류가 바뀌었다 — 설 수 있는 대화를 다시 묻고, 지난번 고른 것을 되살린다.
@@ -2402,6 +2575,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2402
2575
  this.askHostForMode(kind);
2403
2576
  }
2404
2577
  updated(changed) {
2578
+ if (changed.has('input') || changed.has('composerWide') || changed.has('previewAttachmentId') || changed.has('attachments'))
2579
+ this.fitComposer();
2405
2580
  /*
2406
2581
  * 밖에서 넣어 준 질문을 자동 전송한다(오류 토스트의 "AI에게 묻기" 등).
2407
2582
  *
@@ -2639,7 +2814,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2639
2814
  ${this.renderModeSubject()}
2640
2815
  ${this.renderSessionTabs()}
2641
2816
  ${this.renderRoster()}
2642
- <div class="messages" @click=${this.onBubbleClick}>
2817
+ <div class="messages ${this.composerWide || this.previewedTable ? 'covered' : ''}" @click=${this.onBubbleClick}>
2643
2818
  ${!hasMessages ? this.renderEmpty() : nothing}
2644
2819
  ${(() => {
2645
2820
  const myEmail = this._myEmail;
@@ -2763,7 +2938,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2763
2938
  : nothing}
2764
2939
  ${this.renderFoldNotice()}
2765
2940
  <div
2766
- class="composer ${this.dragOver ? 'drag-over' : ''}"
2941
+ class="composer ${this.dragOver ? 'drag-over' : ''} ${this.composerWide ? 'wide' : ''} ${this.previewedTable ? 'previewing' : ''}"
2767
2942
  @dragover=${(e) => this.onComposerDragOver(e)}
2768
2943
  @dragleave=${(e) => this.onComposerDragLeave(e)}
2769
2944
  @drop=${(e) => this.onComposerDrop(e)}
@@ -2815,20 +2990,43 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2815
2990
  </button>
2816
2991
  `
2817
2992
  : nothing}
2993
+ <button
2994
+ type="button"
2995
+ class="action composer-size"
2996
+ aria-pressed=${this.composerWide ? 'true' : 'false'}
2997
+ @click=${() => this.setComposerWide(!this.composerWide)}>
2998
+ <md-icon>${this.composerWide ? 'close_fullscreen' : 'open_in_full'}</md-icon>
2999
+ ${this.composerWide
3000
+ ? i18next.t('ai-assistant.button.write-narrow', { defaultValue: 'Narrow' })
3001
+ : i18next.t('ai-assistant.button.write-wide', { defaultValue: 'Write wide' })}
3002
+ </button>
2818
3003
  </div>
3004
+ ${this.renderTablePreview()}
2819
3005
  ${this.examplesOpen ? this.renderInlineExamples() : nothing}
2820
3006
  ${this.attachments.length > 0
2821
3007
  ? html `
2822
3008
  <div class="attachment-strip">
2823
3009
  ${this.attachments.map(att => html `
2824
- <div class="attachment-chip" title=${att.name}>
2825
- ${att.type.startsWith('image/')
2826
- ? html `<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
2827
- : html `<md-icon class="attachment-icon">description</md-icon>`}
2828
- <div class="attachment-meta">
2829
- <span class="attachment-name">${att.name}</span>
2830
- <span class="attachment-size">${formatFileSize(att.size)}</span>
2831
- </div>
3010
+ <div class="attachment-chip ${att.id === this.previewAttachmentId ? 'open' : ''}" title=${att.name}>
3011
+ ${att.table
3012
+ ? html `<button
3013
+ type="button"
3014
+ class="attachment-open"
3015
+ aria-expanded=${att.id === this.previewAttachmentId ? 'true' : 'false'}
3016
+ @click=${() => (this.previewAttachmentId = att.id === this.previewAttachmentId ? null : att.id)}>
3017
+ <md-icon class="attachment-icon">table_chart</md-icon>
3018
+ <span class="attachment-meta">
3019
+ <span class="attachment-name">${att.name}</span>
3020
+ <span class="attachment-size">${this.tableShape(att.table)}</span>
3021
+ </span>
3022
+ </button>`
3023
+ : html `${att.type.startsWith('image/')
3024
+ ? html `<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
3025
+ : html `<md-icon class="attachment-icon">description</md-icon>`}
3026
+ <div class="attachment-meta">
3027
+ <span class="attachment-name">${att.name}</span>
3028
+ <span class="attachment-size">${formatFileSize(att.size)}</span>
3029
+ </div>`}
2832
3030
  <button
2833
3031
  type="button"
2834
3032
  class="attachment-remove"
@@ -2898,10 +3096,16 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2898
3096
  </button>
2899
3097
  `
2900
3098
  : nothing}
3099
+ ${this.composerWide
3100
+ ? html `<span class="send-shortcut">${i18next.t('ai-assistant.text.send-shortcut', { key: sendShortcut(navigator.platform), defaultValue: '{key} to send' })}</span>`
3101
+ : nothing}
2901
3102
  <button
2902
3103
  ?disabled=${this.busy || !!this.sendBlocked || (!this.input.trim() && this.attachments.length === 0)}
2903
3104
  @click=${this.send}
2904
- title=${this.sendBlocked || i18next.t('ai-assistant.text.send-tooltip')}>
3105
+ title=${this.sendBlocked ||
3106
+ (this.composerWide
3107
+ ? i18next.t('ai-assistant.text.send-tooltip-wide', { key: sendShortcut(navigator.platform), defaultValue: 'Send ({key})' })
3108
+ : i18next.t('ai-assistant.text.send-tooltip'))}>
2905
3109
  ${this.busy
2906
3110
  ? html `<span class="spinner"></span>`
2907
3111
  : html `<md-icon>arrow_upward</md-icon>`}
@@ -2986,6 +3190,56 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2986
3190
  </div>
2987
3191
  `;
2988
3192
  }
3193
+ get previewedTable() {
3194
+ return this.previewAttachmentId ? this.attachments.find(a => a.id === this.previewAttachmentId && a.table) : undefined;
3195
+ }
3196
+ tableShape(table) {
3197
+ return i18next.t('ai-assistant.text.table-shape', { rows: table.rows.length, columns: table.columns, defaultValue: '{rows} rows × {columns} columns' });
3198
+ }
3199
+ /*
3200
+ The rows of a pasted table, over the conversation - numbered as the file numbers them, which is the number the
3201
+ entry card names. Every row: the paste is held under the mode's limit already.
3202
+ */
3203
+ renderTablePreview() {
3204
+ const att = this.previewedTable;
3205
+ if (!att?.table)
3206
+ return nothing;
3207
+ const { rows, columns } = att.table;
3208
+ return html `
3209
+ <div
3210
+ class="table-preview"
3211
+ role="region"
3212
+ aria-label=${att.name}
3213
+ @keydown=${(e) => {
3214
+ if (e.key === 'Escape') {
3215
+ e.stopPropagation();
3216
+ this.previewAttachmentId = null;
3217
+ }
3218
+ }}>
3219
+ <div class="table-preview-head">
3220
+ <md-icon>table_chart</md-icon>
3221
+ <span class="table-preview-name">${att.name}</span>
3222
+ <span class="table-preview-shape">${this.tableShape(att.table)}</span>
3223
+ <button type="button" class="action" @click=${() => this.tableToWords(att.id)}>
3224
+ <md-icon>notes</md-icon>
3225
+ ${i18next.t('ai-assistant.button.table-to-words', { defaultValue: 'Put in as words' })}
3226
+ </button>
3227
+ <button type="button" class="action" @click=${() => (this.previewAttachmentId = null)}>
3228
+ <md-icon>close</md-icon>
3229
+ ${i18next.t('ai-assistant.button.close')}
3230
+ </button>
3231
+ </div>
3232
+ <div class="table-preview-body">
3233
+ <table>
3234
+ ${rows.map((row, i) => html `<tr>
3235
+ <th scope="row">${i + 1}</th>
3236
+ ${Array.from({ length: columns }, (_, c) => html `<td>${row[c] ?? ''}</td>`)}
3237
+ </tr>`)}
3238
+ </table>
3239
+ </div>
3240
+ </div>
3241
+ `;
3242
+ }
2989
3243
  onKeyDown(e) {
2990
3244
  /*
2991
3245
  The Enter that ends a Korean (or Japanese, Chinese) syllable belongs to the input method - sending on it
@@ -3029,9 +3283,17 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3029
3283
  return;
3030
3284
  }
3031
3285
  }
3032
- if (e.key === 'Enter' && !e.shiftKey && !this.busy) {
3286
+ const action = composerKeyAction(e, this.composerWide);
3287
+ if (action === 'narrow') {
3288
+ /* The Escape that narrows the box is spent here - the floating dock closes on the next one. */
3033
3289
  e.preventDefault();
3034
- this.send();
3290
+ e.stopPropagation();
3291
+ this.setComposerWide(false);
3292
+ }
3293
+ else if (action === 'send') {
3294
+ e.preventDefault();
3295
+ if (!this.busy)
3296
+ this.send();
3035
3297
  }
3036
3298
  }
3037
3299
  /** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
@@ -4198,6 +4460,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4198
4460
  if ((!text && attachmentsToSend.length === 0) || this.busy)
4199
4461
  return;
4200
4462
  this.attachments = [];
4463
+ /* What was written wide is sent to be answered - the conversation comes back to show it. */
4464
+ this.composerWide = false;
4465
+ this.previewAttachmentId = null;
4201
4466
  const promptText = text || (attachmentsToSend.length > 0
4202
4467
  ? (attachmentsToSend[0].type.startsWith('image/')
4203
4468
  ? i18next.t('ai-assistant.text.attached-image-ask', { defaultValue: 'here is an image — go by it.' })
@@ -4574,6 +4839,14 @@ __decorate([
4574
4839
  state(),
4575
4840
  __metadata("design:type", Object)
4576
4841
  ], OxAssistantChat.prototype, "dragOver", void 0);
4842
+ __decorate([
4843
+ state(),
4844
+ __metadata("design:type", Object)
4845
+ ], OxAssistantChat.prototype, "composerWide", void 0);
4846
+ __decorate([
4847
+ state(),
4848
+ __metadata("design:type", Object)
4849
+ ], OxAssistantChat.prototype, "previewAttachmentId", void 0);
4577
4850
  __decorate([
4578
4851
  state(),
4579
4852
  __metadata("design:type", Object)