@@ -7,9 +7,20 @@ import { NcTextField, NcButton, NcModal } from '@nextcloud/vue'
77import { t } from ' @nextcloud/l10n'
88import { ScheduleTask } from ' ../services/assistant/api'
99import { parseMermaidToExcalidraw } from ' @excalidraw/mermaid-to-excalidraw'
10- import { convertToExcalidrawElements } from ' @nextcloud/excalidraw'
10+ import { convertToExcalidrawElements , getCommonBounds } from ' @nextcloud/excalidraw'
1111import { defineComponent } from ' vue'
1212
13+ const AI_GENERATED_TYPE = ' assistant-mermaid'
14+ const WHITEBOARD_DEFAULT_FONT_FAMILY = 3
15+ const WHITEBOARD_DEFAULT_FONT = ' Cascadia, monospace'
16+ const AI_DISCLOSURE_LABEL_GAP = 16
17+ const AI_DISCLOSURE_LABEL_FONT_FAMILY = 2
18+ const AI_DISCLOSURE_LABEL_FONT = ' Helvetica, Arial, sans-serif'
19+ const AI_DISCLOSURE_LABEL_FONT_SIZE = 14
20+ const AI_DISCLOSURE_LABEL_MIN_WIDTH = 280
21+ const AI_DISCLOSURE_LABEL_MAX_WIDTH = 440
22+ const AI_DISCLOSURE_LABEL_FONT_STRING = ` ${ AI_DISCLOSURE_LABEL_FONT_SIZE } px ${ AI_DISCLOSURE_LABEL_FONT } `
23+
1324export default defineComponent ({
1425 name: ' AssistantDialog' ,
1526 components: {
@@ -40,29 +51,173 @@ export default defineComponent({
4051 },
4152 methods: {
4253 t,
54+ createAssistantMetadata (extraData = {}) {
55+ return {
56+ aiGenerated: AI_GENERATED_TYPE ,
57+ ... extraData,
58+ }
59+ },
60+ getAssistantDisclosureText () {
61+ return t (' whiteboard' , ' Output is generated by AI. Make sure to always double-check.' )
62+ },
63+ getDiagramDisclosureText () {
64+ return t (' whiteboard' , ' Diagram shown here is generated by AI. Make sure to always double-check.' )
65+ },
4366 onCancel () {
4467 this .show = false
4568 this .$emit (' cancel' )
4669 },
47- async getExcalidrawElements (taskResponse ) {
48- this .mermaidError = false
49-
50- const { elements , files } = await parseMermaidToExcalidraw (taskResponse)
70+ markAssistantGeneratedElements (elements ) {
71+ return elements .map ((element ) => ({
72+ ... element,
73+ customData: {
74+ ... element .customData ,
75+ ... this .createAssistantMetadata (),
76+ },
77+ }))
78+ },
79+ normalizeAssistantElements (elements ) {
5180 elements .forEach ((element ) => {
52- // set font family (6 should always be Nunito)
5381 if (element .label ) {
54- element .label .fontFamily = 6
82+ element .label .fontFamily = WHITEBOARD_DEFAULT_FONT_FAMILY
5583 }
5684 if (element .type === ' text' ) {
57- element .fontFamily = 6
85+ element .fontFamily = WHITEBOARD_DEFAULT_FONT_FAMILY
5886 }
5987 element .roughness = 0
6088 })
61- const data = {
62- elements: convertToExcalidrawElements (elements, { regenerateIds: true }),
89+ },
90+ measureDisclosureTextWidth (text ) {
91+ if (typeof document === ' undefined' ) {
92+ return text .length * AI_DISCLOSURE_LABEL_FONT_SIZE * 0.55
93+ }
94+
95+ const canvas = document .createElement (' canvas' )
96+ const context = canvas .getContext (' 2d' )
97+
98+ if (! context) {
99+ return text .length * AI_DISCLOSURE_LABEL_FONT_SIZE * 0.55
100+ }
101+
102+ context .font = AI_DISCLOSURE_LABEL_FONT_STRING
103+ return context .measureText (text).width
104+ },
105+ splitDisclosureWord (word , maxWidth ) {
106+ const segments = []
107+ let currentSegment = ' '
108+
109+ for (const character of word) {
110+ const nextSegment = ` ${ currentSegment}${ character} `
111+
112+ if (currentSegment && this .measureDisclosureTextWidth (nextSegment) > maxWidth) {
113+ segments .push (currentSegment)
114+ currentSegment = character
115+ continue
116+ }
117+
118+ currentSegment = nextSegment
119+ }
120+
121+ if (currentSegment) {
122+ segments .push (currentSegment)
123+ }
124+
125+ return segments
126+ },
127+ wrapDisclosureText (text , maxWidth ) {
128+ const words = text .split (/ \s + / ).filter (Boolean )
129+
130+ if (! words .length ) {
131+ return text
132+ }
133+
134+ const lines = []
135+ let currentLine = ' '
136+
137+ words .forEach ((word ) => {
138+ const segments = this .measureDisclosureTextWidth (word) > maxWidth
139+ ? this .splitDisclosureWord (word, maxWidth)
140+ : [word]
141+
142+ segments .forEach ((segment , index ) => {
143+ const nextLine = currentLine ? ` ${ currentLine} ${ segment} ` : segment
144+
145+ if (this .measureDisclosureTextWidth (nextLine) <= maxWidth) {
146+ currentLine = nextLine
147+ } else {
148+ if (currentLine) {
149+ lines .push (currentLine)
150+ }
151+ currentLine = segment
152+ }
153+
154+ if (index < segments .length - 1 && currentLine) {
155+ lines .push (currentLine)
156+ currentLine = ' '
157+ }
158+ })
159+ })
160+
161+ if (currentLine) {
162+ lines .push (currentLine)
163+ }
164+
165+ return lines .join (' \n ' )
166+ },
167+ addAssistantDisclosureLabel (elements ) {
168+ if (! elements .length ) {
169+ return elements
170+ }
171+
172+ const [minX , , maxX , maxY ] = getCommonBounds (elements)
173+ const diagramWidth = maxX - minX
174+ const disclosureText = this .wrapDisclosureText (
175+ this .getDiagramDisclosureText (),
176+ Math .min (
177+ AI_DISCLOSURE_LABEL_MAX_WIDTH ,
178+ Math .max (AI_DISCLOSURE_LABEL_MIN_WIDTH , diagramWidth * 0.8 ),
179+ ),
180+ )
181+ const [disclosureElement ] = convertToExcalidrawElements ([{
182+ type: ' text' ,
183+ x: minX,
184+ y: maxY + AI_DISCLOSURE_LABEL_GAP ,
185+ text: disclosureText,
186+ fontSize: AI_DISCLOSURE_LABEL_FONT_SIZE ,
187+ fontFamily: AI_DISCLOSURE_LABEL_FONT_FAMILY ,
188+ textAlign: ' center' ,
189+ backgroundColor: ' transparent' ,
190+ strokeColor: ' #6b7280' ,
191+ opacity: 80 ,
192+ customData: this .createAssistantMetadata ({
193+ aiDisclosureLabel: true ,
194+ }),
195+ }], { regenerateIds: true })
196+
197+ return [
198+ ... elements,
199+ {
200+ ... disclosureElement,
201+ x: minX + ((diagramWidth - disclosureElement .width ) / 2 ),
202+ y: maxY + AI_DISCLOSURE_LABEL_GAP ,
203+ },
204+ ]
205+ },
206+ async getExcalidrawElements (taskResponse ) {
207+ this .mermaidError = false
208+
209+ const { elements , files } = await parseMermaidToExcalidraw (taskResponse, {
210+ fontFamily: WHITEBOARD_DEFAULT_FONT ,
211+ })
212+ this .normalizeAssistantElements (elements)
213+
214+ const generatedElements = this .markAssistantGeneratedElements (
215+ convertToExcalidrawElements (elements, { regenerateIds: true }),
216+ )
217+ return {
218+ elements: this .addAssistantDisclosureLabel (generatedElements),
63219 files,
64220 }
65- return data
66221 },
67222 async onGetTask () {
68223 this .waitingForTask = true
@@ -96,6 +251,12 @@ export default defineComponent({
96251 <h2 >
97252 {{ t('whiteboard', 'Generate diagram') }}
98253 </h2 >
254+ <div class =" assistant-disclosure" >
255+ <span class =" assistant-disclosure__badge" >AI</span >
256+ <p class =" assistant-disclosure__text" >
257+ {{ getAssistantDisclosureText() }}
258+ </p >
259+ </div >
99260 <div v-if =" mermaidError" class =" mermaid-error" >
100261 {{ t('whiteboard', 'Something went wrong, please try again') }}
101262 </div >
@@ -123,27 +284,65 @@ export default defineComponent({
123284.loading-icon {
124285 padding : 30px ;
125286}
287+
126288h2 {
127- text-align : center ;
289+ text-align : center ;
128290}
291+
129292.assistant-dialog {
130- display : flex ;
131- flex-direction : column ;
132- justify-content : space-evenly ;
133- overflow : hidden ;
293+ display : flex ;
294+ flex-direction : column ;
295+ justify-content : space-evenly ;
296+ overflow : hidden ;
297+
134298 form {
135299 padding-inline : 10px ;
136300 }
137301}
302+
303+ .assistant-disclosure {
304+ display : flex ;
305+ align-items : flex-start ;
306+ gap : 10px ;
307+ margin : 0 10px 14px ;
308+ padding : 8px 10px ;
309+ border : 1px solid transparent ;
310+ border-radius : 6px ;
311+ background :
312+ linear-gradient (var (--color-background-assistant ), var (--color-background-assistant )) padding-box ,
313+ var (--color-border-assistant ) border-box ;
314+ color : var (--color-main-text );
315+ font-size : var (--font-size-small );
316+ line-height : 1.35 ;
317+ }
318+
319+ .assistant-disclosure__badge {
320+ flex : none ;
321+ min-width : 28px ;
322+ padding : 2px 6px ;
323+ border-radius : 6px ;
324+ background : var (--color-border-assistant );
325+ font-size : 11px ;
326+ font-weight : 700 ;
327+ line-height : 1.4 ;
328+ text-align : center ;
329+ }
330+
331+ .assistant-disclosure__text {
332+ margin : 0 ;
333+ font-weight : 500 ;
334+ }
335+
138336.dialog-buttons {
139- display : flex ;
140- justify-content : space-between ;
141- align-items : center ;
142- width : 100% ;
337+ display : flex ;
338+ justify-content : space-between ;
339+ align-items : center ;
340+ width : 100% ;
143341 padding-block : 10px ;
144342}
343+
145344.mermaid-error {
146345 text-align : center ;
147- font-weight : bold ;
346+ font-weight : bold ;
148347}
149348 </style >
0 commit comments