@things-factory/ai-assistant 10.1.109 → 10.1.110

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,115 @@ 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
+ .send-shortcut {
2086
+ font-size: 10px;
2087
+ color: var(--md-sys-color-outline, #64748b);
2088
+ white-space: nowrap;
2089
+ padding-bottom: 10px;
2090
+ }
2091
+ .table-preview {
2092
+ flex: 1;
2093
+ min-height: 0;
2094
+ display: flex;
2095
+ flex-direction: column;
2096
+ margin: 4px 14px;
2097
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2098
+ border-radius: 8px;
2099
+ background: var(--md-sys-color-surface, #ffffff);
2100
+ overflow: hidden;
2101
+ }
2102
+ .table-preview-head {
2103
+ display: flex;
2104
+ align-items: center;
2105
+ gap: 6px;
2106
+ padding: 6px 8px;
2107
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2108
+ flex-wrap: wrap;
2109
+ }
2110
+ .table-preview-head > md-icon {
2111
+ --md-icon-size: 18px;
2112
+ color: var(--md-sys-color-primary, #0284c7);
2113
+ }
2114
+ .table-preview-name {
2115
+ font-weight: 500;
2116
+ color: var(--md-sys-color-on-surface, #0f172a);
2117
+ min-width: 0;
2118
+ overflow: hidden;
2119
+ text-overflow: ellipsis;
2120
+ white-space: nowrap;
2121
+ }
2122
+ .table-preview-shape {
2123
+ color: var(--md-sys-color-on-surface-variant, #475569);
2124
+ margin-right: auto;
2125
+ }
2126
+ .table-preview-body {
2127
+ flex: 1;
2128
+ min-height: 0;
2129
+ overflow: auto;
2130
+ }
2131
+ .table-preview-body table {
2132
+ border-collapse: collapse;
2133
+ font-size: 11px;
2134
+ }
2135
+ .table-preview-body th,
2136
+ .table-preview-body td {
2137
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2138
+ padding: 4px 8px;
2139
+ text-align: left;
2140
+ white-space: nowrap;
2141
+ color: var(--md-sys-color-on-surface, #0f172a);
2142
+ }
2143
+ .table-preview-body th {
2144
+ position: sticky;
2145
+ left: 0;
2146
+ font-weight: 400;
2147
+ text-align: right;
2148
+ color: var(--md-sys-color-on-surface-variant, #475569);
2149
+ background: var(--md-sys-color-surface-container, #f1f5f9);
2150
+ }
2151
+ .attachment-open {
2152
+ all: unset;
2153
+ display: flex;
2154
+ align-items: center;
2155
+ gap: 6px;
2156
+ min-width: 0;
2157
+ flex: 1;
2158
+ cursor: pointer;
2159
+ border-radius: 6px;
2160
+ }
2161
+ .attachment-open:focus-visible {
2162
+ outline: 2px solid var(--md-sys-color-primary, #0284c7);
2163
+ outline-offset: 2px;
2164
+ }
2165
+ .attachment-chip.open {
2166
+ border-color: var(--md-sys-color-primary, #0284c7);
2167
+ }
2018
2168
  .composer.drag-over {
2019
2169
  background: var(--md-sys-color-surface-container, #f1f5f9);
2020
2170
  border-top-color: var(--md-sys-color-primary, #0284c7);
@@ -2281,8 +2431,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2281
2431
  font: inherit;
2282
2432
  letter-spacing: inherit;
2283
2433
  resize: none;
2284
- min-height: 36px;
2285
- max-height: 140px;
2434
+ /* The resting height of one line as it stood; the box grows from here by composerHeight (shared-dock.md §10). */
2435
+ box-sizing: border-box;
2436
+ min-height: 54px;
2286
2437
  outline: none;
2287
2438
  transition:
2288
2439
  border-color 0.2s,
@@ -2402,6 +2553,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2402
2553
  this.askHostForMode(kind);
2403
2554
  }
2404
2555
  updated(changed) {
2556
+ if (changed.has('input') || changed.has('composerWide') || changed.has('previewAttachmentId') || changed.has('attachments'))
2557
+ this.fitComposer();
2405
2558
  /*
2406
2559
  * 밖에서 넣어 준 질문을 자동 전송한다(오류 토스트의 "AI에게 묻기" 등).
2407
2560
  *
@@ -2639,7 +2792,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2639
2792
  ${this.renderModeSubject()}
2640
2793
  ${this.renderSessionTabs()}
2641
2794
  ${this.renderRoster()}
2642
- <div class="messages" @click=${this.onBubbleClick}>
2795
+ <div class="messages ${this.composerWide || this.previewedTable ? 'covered' : ''}" @click=${this.onBubbleClick}>
2643
2796
  ${!hasMessages ? this.renderEmpty() : nothing}
2644
2797
  ${(() => {
2645
2798
  const myEmail = this._myEmail;
@@ -2763,7 +2916,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2763
2916
  : nothing}
2764
2917
  ${this.renderFoldNotice()}
2765
2918
  <div
2766
- class="composer ${this.dragOver ? 'drag-over' : ''}"
2919
+ class="composer ${this.dragOver ? 'drag-over' : ''} ${this.composerWide ? 'wide' : ''} ${this.previewedTable ? 'previewing' : ''}"
2767
2920
  @dragover=${(e) => this.onComposerDragOver(e)}
2768
2921
  @dragleave=${(e) => this.onComposerDragLeave(e)}
2769
2922
  @drop=${(e) => this.onComposerDrop(e)}
@@ -2815,20 +2968,43 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2815
2968
  </button>
2816
2969
  `
2817
2970
  : nothing}
2971
+ <button
2972
+ type="button"
2973
+ class="action composer-size"
2974
+ aria-pressed=${this.composerWide ? 'true' : 'false'}
2975
+ @click=${() => this.setComposerWide(!this.composerWide)}>
2976
+ <md-icon>${this.composerWide ? 'close_fullscreen' : 'open_in_full'}</md-icon>
2977
+ ${this.composerWide
2978
+ ? i18next.t('ai-assistant.button.write-narrow', { defaultValue: 'Narrow' })
2979
+ : i18next.t('ai-assistant.button.write-wide', { defaultValue: 'Write wide' })}
2980
+ </button>
2818
2981
  </div>
2982
+ ${this.renderTablePreview()}
2819
2983
  ${this.examplesOpen ? this.renderInlineExamples() : nothing}
2820
2984
  ${this.attachments.length > 0
2821
2985
  ? html `
2822
2986
  <div class="attachment-strip">
2823
2987
  ${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>
2988
+ <div class="attachment-chip ${att.id === this.previewAttachmentId ? 'open' : ''}" title=${att.name}>
2989
+ ${att.table
2990
+ ? html `<button
2991
+ type="button"
2992
+ class="attachment-open"
2993
+ aria-expanded=${att.id === this.previewAttachmentId ? 'true' : 'false'}
2994
+ @click=${() => (this.previewAttachmentId = att.id === this.previewAttachmentId ? null : att.id)}>
2995
+ <md-icon class="attachment-icon">table_chart</md-icon>
2996
+ <span class="attachment-meta">
2997
+ <span class="attachment-name">${att.name}</span>
2998
+ <span class="attachment-size">${this.tableShape(att.table)}</span>
2999
+ </span>
3000
+ </button>`
3001
+ : html `${att.type.startsWith('image/')
3002
+ ? html `<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
3003
+ : html `<md-icon class="attachment-icon">description</md-icon>`}
3004
+ <div class="attachment-meta">
3005
+ <span class="attachment-name">${att.name}</span>
3006
+ <span class="attachment-size">${formatFileSize(att.size)}</span>
3007
+ </div>`}
2832
3008
  <button
2833
3009
  type="button"
2834
3010
  class="attachment-remove"
@@ -2898,10 +3074,16 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2898
3074
  </button>
2899
3075
  `
2900
3076
  : nothing}
3077
+ ${this.composerWide
3078
+ ? html `<span class="send-shortcut">${i18next.t('ai-assistant.text.send-shortcut', { key: sendShortcut(navigator.platform), defaultValue: '{key} to send' })}</span>`
3079
+ : nothing}
2901
3080
  <button
2902
3081
  ?disabled=${this.busy || !!this.sendBlocked || (!this.input.trim() && this.attachments.length === 0)}
2903
3082
  @click=${this.send}
2904
- title=${this.sendBlocked || i18next.t('ai-assistant.text.send-tooltip')}>
3083
+ title=${this.sendBlocked ||
3084
+ (this.composerWide
3085
+ ? i18next.t('ai-assistant.text.send-tooltip-wide', { key: sendShortcut(navigator.platform), defaultValue: 'Send ({key})' })
3086
+ : i18next.t('ai-assistant.text.send-tooltip'))}>
2905
3087
  ${this.busy
2906
3088
  ? html `<span class="spinner"></span>`
2907
3089
  : html `<md-icon>arrow_upward</md-icon>`}
@@ -2986,6 +3168,47 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2986
3168
  </div>
2987
3169
  `;
2988
3170
  }
3171
+ get previewedTable() {
3172
+ return this.previewAttachmentId ? this.attachments.find(a => a.id === this.previewAttachmentId && a.table) : undefined;
3173
+ }
3174
+ tableShape(table) {
3175
+ return i18next.t('ai-assistant.text.table-shape', { rows: table.rows.length, columns: table.columns, defaultValue: '{rows} rows × {columns} columns' });
3176
+ }
3177
+ /*
3178
+ The rows of a pasted table, over the conversation - numbered as the file numbers them, which is the number the
3179
+ entry card names. Every row: the paste is held under the mode's limit already.
3180
+ */
3181
+ renderTablePreview() {
3182
+ const att = this.previewedTable;
3183
+ if (!att?.table)
3184
+ return nothing;
3185
+ const { rows, columns } = att.table;
3186
+ return html `
3187
+ <div class="table-preview" role="region" aria-label=${att.name}>
3188
+ <div class="table-preview-head">
3189
+ <md-icon>table_chart</md-icon>
3190
+ <span class="table-preview-name">${att.name}</span>
3191
+ <span class="table-preview-shape">${this.tableShape(att.table)}</span>
3192
+ <button type="button" class="action" @click=${() => this.tableToWords(att.id)}>
3193
+ <md-icon>notes</md-icon>
3194
+ ${i18next.t('ai-assistant.button.table-to-words', { defaultValue: 'Put in as words' })}
3195
+ </button>
3196
+ <button type="button" class="action" @click=${() => (this.previewAttachmentId = null)}>
3197
+ <md-icon>close</md-icon>
3198
+ ${i18next.t('ai-assistant.button.close')}
3199
+ </button>
3200
+ </div>
3201
+ <div class="table-preview-body">
3202
+ <table>
3203
+ ${rows.map((row, i) => html `<tr>
3204
+ <th scope="row">${i + 1}</th>
3205
+ ${Array.from({ length: columns }, (_, c) => html `<td>${row[c] ?? ''}</td>`)}
3206
+ </tr>`)}
3207
+ </table>
3208
+ </div>
3209
+ </div>
3210
+ `;
3211
+ }
2989
3212
  onKeyDown(e) {
2990
3213
  /*
2991
3214
  The Enter that ends a Korean (or Japanese, Chinese) syllable belongs to the input method - sending on it
@@ -3029,9 +3252,15 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3029
3252
  return;
3030
3253
  }
3031
3254
  }
3032
- if (e.key === 'Enter' && !e.shiftKey && !this.busy) {
3255
+ const action = composerKeyAction(e, this.composerWide);
3256
+ if (action === 'narrow') {
3033
3257
  e.preventDefault();
3034
- this.send();
3258
+ this.setComposerWide(false);
3259
+ }
3260
+ else if (action === 'send') {
3261
+ e.preventDefault();
3262
+ if (!this.busy)
3263
+ this.send();
3035
3264
  }
3036
3265
  }
3037
3266
  /** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
@@ -4198,6 +4427,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4198
4427
  if ((!text && attachmentsToSend.length === 0) || this.busy)
4199
4428
  return;
4200
4429
  this.attachments = [];
4430
+ /* What was written wide is sent to be answered - the conversation comes back to show it. */
4431
+ this.composerWide = false;
4432
+ this.previewAttachmentId = null;
4201
4433
  const promptText = text || (attachmentsToSend.length > 0
4202
4434
  ? (attachmentsToSend[0].type.startsWith('image/')
4203
4435
  ? i18next.t('ai-assistant.text.attached-image-ask', { defaultValue: 'here is an image — go by it.' })
@@ -4574,6 +4806,14 @@ __decorate([
4574
4806
  state(),
4575
4807
  __metadata("design:type", Object)
4576
4808
  ], OxAssistantChat.prototype, "dragOver", void 0);
4809
+ __decorate([
4810
+ state(),
4811
+ __metadata("design:type", Object)
4812
+ ], OxAssistantChat.prototype, "composerWide", void 0);
4813
+ __decorate([
4814
+ state(),
4815
+ __metadata("design:type", Object)
4816
+ ], OxAssistantChat.prototype, "previewAttachmentId", void 0);
4577
4817
  __decorate([
4578
4818
  state(),
4579
4819
  __metadata("design:type", Object)