|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import { useCallback, useEffect, useState } from 'react' |
| 3 | +import { useCallback, useEffect, useRef, useState } from 'react' |
4 | 4 | import { createLogger } from '@sim/logger' |
5 | 5 | import { format } from 'date-fns' |
6 | 6 | import { |
@@ -406,12 +406,22 @@ export function KnowledgeBase({ |
406 | 406 | }: KnowledgeBaseProps) { |
407 | 407 | const params = useParams() |
408 | 408 | const workspaceId = params.workspaceId as string |
409 | | - const { removeKnowledgeBase } = useKnowledgeBasesList(workspaceId, { enabled: false }) |
| 409 | + const { removeKnowledgeBase, refreshList } = useKnowledgeBasesList(workspaceId, { |
| 410 | + enabled: false, |
| 411 | + }) |
410 | 412 | const userPermissions = useUserPermissionsContext() |
411 | 413 |
|
412 | 414 | const [searchQuery, setSearchQuery] = useState('') |
413 | 415 | const [showTagsModal, setShowTagsModal] = useState(false) |
414 | 416 |
|
| 417 | + const [isEditingName, setIsEditingName] = useState(false) |
| 418 | + const [editName, setEditName] = useState('') |
| 419 | + const [isEditingDescription, setIsEditingDescription] = useState(false) |
| 420 | + const [editDescription, setEditDescription] = useState('') |
| 421 | + const [isSaving, setIsSaving] = useState(false) |
| 422 | + const nameInputRef = useRef<HTMLInputElement>(null) |
| 423 | + const descriptionInputRef = useRef<HTMLTextAreaElement>(null) |
| 424 | + |
415 | 425 | /** |
416 | 426 | * Memoize the search query setter to prevent unnecessary re-renders |
417 | 427 | */ |
@@ -460,6 +470,168 @@ export function KnowledgeBase({ |
460 | 470 | const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base' |
461 | 471 | const error = knowledgeBaseError || documentsError |
462 | 472 |
|
| 473 | + /** |
| 474 | + * Start editing the knowledge base name |
| 475 | + */ |
| 476 | + const handleStartEditName = useCallback(() => { |
| 477 | + setEditName(knowledgeBaseName) |
| 478 | + setIsEditingName(true) |
| 479 | + }, [knowledgeBaseName]) |
| 480 | + |
| 481 | + /** |
| 482 | + * Start editing the knowledge base description |
| 483 | + */ |
| 484 | + const handleStartEditDescription = useCallback(() => { |
| 485 | + setEditDescription(knowledgeBase?.description || '') |
| 486 | + setIsEditingDescription(true) |
| 487 | + }, [knowledgeBase?.description]) |
| 488 | + |
| 489 | + /** |
| 490 | + * Save the updated name |
| 491 | + */ |
| 492 | + const handleSaveName = useCallback(async () => { |
| 493 | + const trimmedName = editName.trim() |
| 494 | + |
| 495 | + if (!trimmedName || trimmedName === knowledgeBaseName) { |
| 496 | + setIsEditingName(false) |
| 497 | + setEditName('') |
| 498 | + return |
| 499 | + } |
| 500 | + |
| 501 | + try { |
| 502 | + setIsSaving(true) |
| 503 | + const response = await fetch(`/api/knowledge/${id}`, { |
| 504 | + method: 'PUT', |
| 505 | + headers: { |
| 506 | + 'Content-Type': 'application/json', |
| 507 | + }, |
| 508 | + body: JSON.stringify({ name: trimmedName }), |
| 509 | + }) |
| 510 | + |
| 511 | + if (!response.ok) { |
| 512 | + throw new Error('Failed to update knowledge base name') |
| 513 | + } |
| 514 | + |
| 515 | + await refreshKnowledgeBase() |
| 516 | + await refreshList() |
| 517 | + setIsEditingName(false) |
| 518 | + setEditName('') |
| 519 | + logger.info(`Successfully updated knowledge base name to: ${trimmedName}`) |
| 520 | + } catch (err) { |
| 521 | + logger.error('Error updating knowledge base name:', err) |
| 522 | + setEditName(knowledgeBaseName) |
| 523 | + } finally { |
| 524 | + setIsSaving(false) |
| 525 | + } |
| 526 | + }, [editName, knowledgeBaseName, id, refreshKnowledgeBase, refreshList]) |
| 527 | + |
| 528 | + /** |
| 529 | + * Save the updated description |
| 530 | + */ |
| 531 | + const handleSaveDescription = useCallback(async () => { |
| 532 | + const trimmedDescription = editDescription.trim() |
| 533 | + const currentDescription = knowledgeBase?.description || '' |
| 534 | + |
| 535 | + if (trimmedDescription === currentDescription) { |
| 536 | + setIsEditingDescription(false) |
| 537 | + setEditDescription('') |
| 538 | + return |
| 539 | + } |
| 540 | + |
| 541 | + try { |
| 542 | + setIsSaving(true) |
| 543 | + const response = await fetch(`/api/knowledge/${id}`, { |
| 544 | + method: 'PUT', |
| 545 | + headers: { |
| 546 | + 'Content-Type': 'application/json', |
| 547 | + }, |
| 548 | + body: JSON.stringify({ description: trimmedDescription }), |
| 549 | + }) |
| 550 | + |
| 551 | + if (!response.ok) { |
| 552 | + throw new Error('Failed to update knowledge base description') |
| 553 | + } |
| 554 | + |
| 555 | + await refreshKnowledgeBase() |
| 556 | + setIsEditingDescription(false) |
| 557 | + setEditDescription('') |
| 558 | + logger.info(`Successfully updated knowledge base description`) |
| 559 | + } catch (err) { |
| 560 | + logger.error('Error updating knowledge base description:', err) |
| 561 | + setEditDescription(knowledgeBase?.description || '') |
| 562 | + } finally { |
| 563 | + setIsSaving(false) |
| 564 | + } |
| 565 | + }, [editDescription, knowledgeBase?.description, id, refreshKnowledgeBase]) |
| 566 | + |
| 567 | + /** |
| 568 | + * Cancel editing name |
| 569 | + */ |
| 570 | + const handleCancelEditName = useCallback(() => { |
| 571 | + setIsEditingName(false) |
| 572 | + setEditName('') |
| 573 | + }, []) |
| 574 | + |
| 575 | + /** |
| 576 | + * Cancel editing description |
| 577 | + */ |
| 578 | + const handleCancelEditDescription = useCallback(() => { |
| 579 | + setIsEditingDescription(false) |
| 580 | + setEditDescription('') |
| 581 | + }, []) |
| 582 | + |
| 583 | + /** |
| 584 | + * Handle keyboard events for name input |
| 585 | + */ |
| 586 | + const handleNameKeyDown = useCallback( |
| 587 | + (e: React.KeyboardEvent) => { |
| 588 | + if (e.key === 'Enter') { |
| 589 | + e.preventDefault() |
| 590 | + handleSaveName() |
| 591 | + } else if (e.key === 'Escape') { |
| 592 | + e.preventDefault() |
| 593 | + handleCancelEditName() |
| 594 | + } |
| 595 | + }, |
| 596 | + [handleSaveName, handleCancelEditName] |
| 597 | + ) |
| 598 | + |
| 599 | + /** |
| 600 | + * Handle keyboard events for description input |
| 601 | + */ |
| 602 | + const handleDescriptionKeyDown = useCallback( |
| 603 | + (e: React.KeyboardEvent) => { |
| 604 | + if (e.key === 'Enter' && !e.shiftKey) { |
| 605 | + e.preventDefault() |
| 606 | + handleSaveDescription() |
| 607 | + } else if (e.key === 'Escape') { |
| 608 | + e.preventDefault() |
| 609 | + handleCancelEditDescription() |
| 610 | + } |
| 611 | + }, |
| 612 | + [handleSaveDescription, handleCancelEditDescription] |
| 613 | + ) |
| 614 | + |
| 615 | + /** |
| 616 | + * Focus and select name input when editing starts |
| 617 | + */ |
| 618 | + useEffect(() => { |
| 619 | + if (isEditingName && nameInputRef.current) { |
| 620 | + nameInputRef.current.focus() |
| 621 | + nameInputRef.current.select() |
| 622 | + } |
| 623 | + }, [isEditingName]) |
| 624 | + |
| 625 | + /** |
| 626 | + * Focus and select description input when editing starts |
| 627 | + */ |
| 628 | + useEffect(() => { |
| 629 | + if (isEditingDescription && descriptionInputRef.current) { |
| 630 | + descriptionInputRef.current.focus() |
| 631 | + descriptionInputRef.current.select() |
| 632 | + } |
| 633 | + }, [isEditingDescription]) |
| 634 | + |
463 | 635 | const totalPages = Math.ceil(pagination.total / pagination.limit) |
464 | 636 | const hasNextPage = currentPage < totalPages |
465 | 637 | const hasPrevPage = currentPage > 1 |
@@ -991,9 +1163,37 @@ export function KnowledgeBase({ |
991 | 1163 | <Breadcrumb items={breadcrumbItems} /> |
992 | 1164 |
|
993 | 1165 | <div className='mt-[14px] flex items-center justify-between'> |
994 | | - <h1 className='font-medium text-[18px] text-[var(--text-primary)]'> |
995 | | - {knowledgeBaseName} |
996 | | - </h1> |
| 1166 | + {isEditingName ? ( |
| 1167 | + <div className='relative inline-flex'> |
| 1168 | + <span |
| 1169 | + className='invisible whitespace-pre font-medium text-[18px]' |
| 1170 | + aria-hidden='true' |
| 1171 | + > |
| 1172 | + {editName || '\u00A0'} |
| 1173 | + </span> |
| 1174 | + <input |
| 1175 | + ref={nameInputRef} |
| 1176 | + value={editName} |
| 1177 | + onChange={(e) => setEditName(e.target.value)} |
| 1178 | + onKeyDown={handleNameKeyDown} |
| 1179 | + onBlur={handleSaveName} |
| 1180 | + className='absolute top-0 left-0 h-full w-full border-0 bg-transparent p-0 font-medium text-[18px] text-[var(--text-primary)] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0' |
| 1181 | + maxLength={200} |
| 1182 | + disabled={isSaving} |
| 1183 | + autoComplete='off' |
| 1184 | + autoCorrect='off' |
| 1185 | + autoCapitalize='off' |
| 1186 | + spellCheck='false' |
| 1187 | + /> |
| 1188 | + </div> |
| 1189 | + ) : ( |
| 1190 | + <h1 |
| 1191 | + className={`font-medium text-[18px] text-[var(--text-primary)] ${userPermissions.canEdit ? 'cursor-text' : ''}`} |
| 1192 | + onDoubleClick={userPermissions.canEdit ? handleStartEditName : undefined} |
| 1193 | + > |
| 1194 | + {knowledgeBaseName} |
| 1195 | + </h1> |
| 1196 | + )} |
997 | 1197 | <div className='flex items-center gap-2'> |
998 | 1198 | {userPermissions.canEdit && ( |
999 | 1199 | <Button |
@@ -1023,11 +1223,45 @@ export function KnowledgeBase({ |
1023 | 1223 | </div> |
1024 | 1224 | </div> |
1025 | 1225 |
|
1026 | | - {knowledgeBase?.description && ( |
1027 | | - <p className='mt-[4px] line-clamp-2 max-w-[40vw] font-medium text-[14px] text-[var(--text-tertiary)]'> |
| 1226 | + {isEditingDescription ? ( |
| 1227 | + <div className='relative mt-[4px] inline-flex max-w-[40vw]'> |
| 1228 | + <span |
| 1229 | + className='invisible whitespace-pre-wrap font-medium text-[14px]' |
| 1230 | + aria-hidden='true' |
| 1231 | + > |
| 1232 | + {editDescription || 'Add a description...'} |
| 1233 | + </span> |
| 1234 | + <textarea |
| 1235 | + ref={descriptionInputRef} |
| 1236 | + value={editDescription} |
| 1237 | + onChange={(e) => setEditDescription(e.target.value)} |
| 1238 | + onKeyDown={handleDescriptionKeyDown} |
| 1239 | + onBlur={handleSaveDescription} |
| 1240 | + className='absolute top-0 left-0 h-full w-full resize-none border-0 bg-transparent p-0 font-medium text-[14px] text-[var(--text-tertiary)] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0' |
| 1241 | + maxLength={500} |
| 1242 | + disabled={isSaving} |
| 1243 | + autoComplete='off' |
| 1244 | + autoCorrect='off' |
| 1245 | + autoCapitalize='off' |
| 1246 | + spellCheck='false' |
| 1247 | + rows={2} |
| 1248 | + /> |
| 1249 | + </div> |
| 1250 | + ) : knowledgeBase?.description ? ( |
| 1251 | + <p |
| 1252 | + className={`mt-[4px] line-clamp-2 max-w-[40vw] font-medium text-[14px] text-[var(--text-tertiary)] ${userPermissions.canEdit ? 'cursor-text' : ''}`} |
| 1253 | + onDoubleClick={userPermissions.canEdit ? handleStartEditDescription : undefined} |
| 1254 | + > |
1028 | 1255 | {knowledgeBase.description} |
1029 | 1256 | </p> |
1030 | | - )} |
| 1257 | + ) : userPermissions.canEdit ? ( |
| 1258 | + <p |
| 1259 | + className='mt-[4px] cursor-text font-medium text-[14px] text-[var(--text-muted)] transition-colors hover:text-[var(--text-tertiary)]' |
| 1260 | + onDoubleClick={handleStartEditDescription} |
| 1261 | + > |
| 1262 | + Add a description... |
| 1263 | + </p> |
| 1264 | + ) : null} |
1031 | 1265 |
|
1032 | 1266 | <div className='mt-[16px] flex items-center gap-[8px]'> |
1033 | 1267 | <span className='text-[14px] text-[var(--text-muted)]'> |
|
0 commit comments