diff --git a/client/src/CommandBar/Body/Item.tsx b/client/src/CommandBar/Body/Item.tsx index 051ca33ba1..bebfb70bc5 100644 --- a/client/src/CommandBar/Body/Item.tsx +++ b/client/src/CommandBar/Body/Item.tsx @@ -84,10 +84,7 @@ const CommandBarItem = ({ } } else { setChosenStep({ - id: id as Exclude< - CommandBarStepEnum, - CommandBarStepEnum.ADD_FILE_TO_STUDIO - >, + id: id as Exclude, }); } updateArrayInStorage(RECENT_COMMANDS_KEY, itemKey); diff --git a/client/src/CommandBar/index.tsx b/client/src/CommandBar/index.tsx index 3018999df2..35735c1daf 100644 --- a/client/src/CommandBar/index.tsx +++ b/client/src/CommandBar/index.tsx @@ -15,7 +15,7 @@ import ManageRepos from './steps/ManageRepos'; import AddNewRepo from './steps/AddNewRepo'; import ToggleTheme from './steps/ToggleTheme'; import SearchFiles from './steps/SeachFiles'; -import AddFileToStudio from './steps/AddFileToStudio'; +import AddFileToStudio from './steps/AddToStudio'; type Props = {}; @@ -79,7 +79,7 @@ const CommandBar = ({}: Props) => { ) : chosenStep.id === CommandBarStepEnum.SEARCH_FILES ? ( - ) : chosenStep.id === CommandBarStepEnum.ADD_FILE_TO_STUDIO ? ( + ) : chosenStep.id === CommandBarStepEnum.ADD_TO_STUDIO ? ( ) : null} diff --git a/client/src/CommandBar/steps/AddFileToStudio.tsx b/client/src/CommandBar/steps/AddToStudio.tsx similarity index 71% rename from client/src/CommandBar/steps/AddFileToStudio.tsx rename to client/src/CommandBar/steps/AddToStudio.tsx index 72e5c11bbc..02c32057f5 100644 --- a/client/src/CommandBar/steps/AddFileToStudio.tsx +++ b/client/src/CommandBar/steps/AddToStudio.tsx @@ -9,6 +9,7 @@ import { } from 'react'; import { useTranslation } from 'react-i18next'; import { + AddDocToStudioDataType, AddFileToStudioDataType, CommandBarItemGeneralType, CommandBarSectionType, @@ -23,10 +24,13 @@ import { CommandBarContext } from '../../context/commandBarContext'; import { ProjectContext } from '../../context/projectContext'; import { TabsContext } from '../../context/tabsContext'; import { postCodeStudio } from '../../services/api'; +import FileIcon from '../../components/FileIcon'; +import TokenUsage from '../../components/TokenUsage'; +import { TOKEN_LIMIT } from '../../consts/codeStudio'; -type Props = AddFileToStudioDataType & {}; +type Props = (AddFileToStudioDataType | AddDocToStudioDataType) & {}; -const AddFileToStudio = ({ path, repoRef, branch }: Props) => { +const AddToStudio = (props: Props) => { const { t } = useTranslation(); const { setChosenStep } = useContext(CommandBarContext.Handlers); const { project, refreshCurrentProjectStudios } = useContext( @@ -50,15 +54,21 @@ const AddFileToStudio = ({ path, repoRef, branch }: Props) => { if (project?.id) { const newId = await postCodeStudio(project.id); refreshCurrentProjectStudios(); - openNewTab({ - type: TabTypesEnum.FILE, - studioId: newId, - path, - repoRef, - branch, - }); + if ('path' in props) { + openNewTab({ + type: TabTypesEnum.FILE, + studioId: newId, + ...props, + }); + } else { + openNewTab({ + type: TabTypesEnum.DOC, + studioId: newId, + ...props, + }); + } } - }, [project?.id, path, repoRef, branch]); + }, [project?.id, props]); const initialSections = useMemo(() => { return [ @@ -81,22 +91,31 @@ const AddFileToStudio = ({ path, repoRef, branch }: Props) => { { items: (project?.studios || []).map((s) => ({ label: s.name, - Icon: CodeStudioIcon, + Icon: () => ( + + ), + iconContainerClassName: 'bg-transparent', id: s.id, key: s.id, onClick: () => - openNewTab({ - type: TabTypesEnum.FILE, - studioId: s.id, - path, - repoRef, - branch, - }), + 'path' in props + ? openNewTab({ + type: TabTypesEnum.FILE, + studioId: s.id, + ...props, + }) + : openNewTab({ + type: TabTypesEnum.DOC, + studioId: s.id, + ...props, + }), closeOnClick: true, - // footerHint: t('{{count}} context files used', { - // count: s.token_counts?.per_file.filter((f) => !!f).length, - // }), - footerHint: '', + footerHint: t('{{count}} context files used', { + count: s.context.length, + }), footerBtns: [{ label: t('Add to existing'), shortcut: ['entr'] }], })), label: t('Existing studio conversations'), @@ -104,15 +123,7 @@ const AddFileToStudio = ({ path, repoRef, branch }: Props) => { key: 'studio-items', }, ]; - }, [ - t, - project?.studios, - handleNewCodeStudio, - openNewTab, - path, - repoRef, - branch, - ]); + }, [t, project?.studios, handleNewCodeStudio, openNewTab, props]); useEffect(() => { if (!inputValue) { @@ -140,8 +151,8 @@ const AddFileToStudio = ({ path, repoRef, branch }: Props) => { }, [initialSections, inputValue]); const breadcrumbs = useMemo(() => { - return [t('Add file to studio')]; - }, []); + return [t(`Add ${'path' in props ? 'file' : 'doc'} to studio`)]; + }, [props, t]); return (
@@ -158,4 +169,4 @@ const AddFileToStudio = ({ path, repoRef, branch }: Props) => { ); }; -export default memo(AddFileToStudio); +export default memo(AddToStudio); diff --git a/client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx b/client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx index 4a99cce624..d3450941df 100644 --- a/client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx +++ b/client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx @@ -1,9 +1,10 @@ -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import DropdownSection from '../../../components/Dropdown/Section'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import { SplitViewIcon, TrashCanIcon } from '../../../icons'; import { deleteConversation } from '../../../services/api'; +import { openInSplitViewShortcut } from '../../../consts/shortcuts'; type Props = { handleMoveToAnotherSide: () => void; @@ -41,20 +42,13 @@ const ActionsDropdown = ({ side, ]); - const shortcuts = useMemo(() => { - return { - splitView: ['cmd', ']'], - }; - }, []); - return (
} /> {conversationId && ( diff --git a/client/src/Project/CurrentTabContent/ChatTab/index.tsx b/client/src/Project/CurrentTabContent/ChatTab/index.tsx index d223c000d1..d714c5d4c6 100644 --- a/client/src/Project/CurrentTabContent/ChatTab/index.tsx +++ b/client/src/Project/CurrentTabContent/ChatTab/index.tsx @@ -19,8 +19,8 @@ import { TabsContext } from '../../../context/tabsContext'; import { ChatTabType } from '../../../types/general'; import { ProjectContext } from '../../../context/projectContext'; import { CommandBarContext } from '../../../context/commandBarContext'; -import { openInSplitViewShortcut } from '../../../consts/commandBar'; import { UIContext } from '../../../context/uiContext'; +import { openInSplitViewShortcut } from '../../../consts/shortcuts'; import Conversation from './Conversation'; import ActionsDropdown from './ActionsDropdown'; @@ -70,7 +70,7 @@ const ChatTab = ({ const handleKeyEvent = useCallback( (e: KeyboardEvent) => { - if (checkEventKeys(e, ['cmd', ']'])) { + if (checkEventKeys(e, openInSplitViewShortcut)) { handleMoveToAnotherSide(); } }, diff --git a/client/src/Project/CurrentTabContent/DocTab/ActionsDropdown.tsx b/client/src/Project/CurrentTabContent/DocTab/ActionsDropdown.tsx new file mode 100644 index 0000000000..dd8b6e58be --- /dev/null +++ b/client/src/Project/CurrentTabContent/DocTab/ActionsDropdown.tsx @@ -0,0 +1,44 @@ +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import DropdownSection from '../../../components/Dropdown/Section'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import { SplitViewIcon, StudioPlusSignIcon } from '../../../icons'; +import { + addToStudioShortcut, + openInSplitViewShortcut, +} from '../../../consts/shortcuts'; + +type Props = { + handleMoveToAnotherSide: () => void; + handleAddToStudio: () => void; +}; + +const ActionsDropdown = ({ + handleMoveToAnotherSide, + handleAddToStudio, +}: Props) => { + const { t } = useTranslation(); + + return ( +
+ + } + /> + + + } + /> + +
+ ); +}; + +export default memo(ActionsDropdown); diff --git a/client/src/Project/CurrentTabContent/DocTab/DocSection.tsx b/client/src/Project/CurrentTabContent/DocTab/DocSection.tsx new file mode 100644 index 0000000000..0ecc526d5f --- /dev/null +++ b/client/src/Project/CurrentTabContent/DocTab/DocSection.tsx @@ -0,0 +1,90 @@ +import React, { + Dispatch, + memo, + SetStateAction, + useCallback, + MouseEvent, +} from 'react'; +import { Trans } from 'react-i18next'; +import Button from '../../../components/Button'; +import { DocSectionType } from '../../../types/api'; +import RenderedSection from './RenderedSection'; + +type Props = DocSectionType & { + isSelected: boolean; + isNothingSelected: boolean; + isEditingSelection: boolean; + setSelectedSections: Dispatch>; +}; + +const DocSection = ({ + text, + isSelected, + setSelectedSections, + point_id, + isNothingSelected, + doc_source, + isEditingSelection, +}: Props) => { + const setSelected = useCallback( + (b: boolean) => { + setSelectedSections((prev) => { + if (b) { + return [...prev, point_id]; + } + return prev.filter((r) => r !== point_id); + }); + }, + [point_id, setSelectedSections], + ); + + const handleClick = useCallback( + (e: MouseEvent) => { + if (isEditingSelection) { + e.stopPropagation(); + setSelected(!isSelected); + } + }, + [isSelected, isEditingSelection], + ); + return ( +
+ {isEditingSelection && ( +
+ +
+ )} + +
+ ); +}; + +export default memo(DocSection); diff --git a/client/src/Project/CurrentTabContent/DocTab/RenderedSection.tsx b/client/src/Project/CurrentTabContent/DocTab/RenderedSection.tsx new file mode 100644 index 0000000000..6c40c09faa --- /dev/null +++ b/client/src/Project/CurrentTabContent/DocTab/RenderedSection.tsx @@ -0,0 +1,61 @@ +import React, { memo, useCallback, useContext, useMemo } from 'react'; +import { Remarkable } from 'remarkable'; +import { highlightCode } from '../../../utils/prism'; +import { DeviceContext } from '../../../context/deviceContext'; + +const md = new Remarkable({ + html: false, + highlight(str: string, lang: string): string { + try { + return highlightCode(str, lang); + } catch (err) { + console.log(err); + return ''; + } + }, + linkTarget: '__blank', +}); + +type Props = { + text: string; + baseUrl: string; + isEditingSelection: boolean; +}; + +const RenderedSection = ({ text, baseUrl, isEditingSelection }: Props) => { + const { openLink } = useContext(DeviceContext); + + const markdown = useMemo(() => md.render(text), [text]); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + // @ts-ignore + const href = e.target.getAttribute('href'); + if (href) { + e.preventDefault(); + e.stopPropagation(); + openLink( + href.startsWith('http://') || href.startsWith('https://') + ? href + : new URL(href, baseUrl).href, + ); + } + }, + [openLink, baseUrl], + ); + + return ( +
+
+
+ ); +}; + +export default memo(RenderedSection); diff --git a/client/src/Project/CurrentTabContent/DocTab/index.tsx b/client/src/Project/CurrentTabContent/DocTab/index.tsx new file mode 100644 index 0000000000..cbd5c233da --- /dev/null +++ b/client/src/Project/CurrentTabContent/DocTab/index.tsx @@ -0,0 +1,387 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { CommandBarStepEnum, DocTabType } from '../../../types/general'; +import { + MagazineIcon, + MoreHorizontalIcon, + SplitViewIcon, +} from '../../../icons'; +import Dropdown from '../../../components/Dropdown'; +import Button from '../../../components/Button'; +import { TabsContext } from '../../../context/tabsContext'; +import { checkEventKeys } from '../../../utils/keyboardUtils'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; +import { UIContext } from '../../../context/uiContext'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import { + addToStudioShortcut, + openInSplitViewShortcut, +} from '../../../consts/shortcuts'; +import { + getCodeStudio, + getDocSections, + getDocTokenCount, + getIndexedPages, + patchCodeStudio, +} from '../../../services/api'; +import { + CodeStudioType, + DocPageType, + DocSectionType, +} from '../../../types/api'; +import { findElementInCurrentTab } from '../../../utils/domUtils'; +import { ProjectContext } from '../../../context/projectContext'; +import Badge from '../../../components/Badge'; +import { humanNumber } from '../../../utils'; +import ActionsDropdown from './ActionsDropdown'; +import DocSection from './DocSection'; + +type Props = DocTabType & { + noBorder?: boolean; + side: 'left' | 'right'; + tabKey: string; + handleMoveToAnotherSide: () => void; +}; + +const DocTab = ({ + side, + tabKey, + handleMoveToAnotherSide, + docId, + title, + favicon, + noBorder, + relativeUrl, + studioId, + initialSections, + isDocInContext, +}: Props) => { + const { t } = useTranslation(); + const { focusedPanel } = useContext(TabsContext.All); + const { updateTabProperty } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const { setFocusedTabItems, setChosenStep, setIsVisible } = useContext( + CommandBarContext.Handlers, + ); + const { isVisible: isCommandBarVisible } = useContext( + CommandBarContext.General, + ); + const { project, refreshCurrentProjectStudios } = useContext( + ProjectContext.Current, + ); + const [fullDoc, setFullDoc] = useState(null); + const [studio, setStudio] = useState(null); + const [sections, setSections] = useState([]); + const [selectedSections, setSelectedSections] = useState( + initialSections || [], + ); + const [tokenCount, setTokenCount] = useState(0); + const [isEditingSelection, setIsEditingSelection] = useState(false); + + const refreshStudio = useCallback(() => { + if (studioId && project?.id) { + getCodeStudio(project.id, studioId).then(setStudio); + } else if (project?.id) { + setStudio(null); + } + }, [studioId, project?.id]); + + useEffect(() => { + refreshStudio(); + }, [refreshStudio]); + + useEffect(() => { + getIndexedPages(docId).then((resp) => { + const doc = resp.find((p) => p.relative_url === relativeUrl); + if (doc) { + setFullDoc(doc); + } + }); + getDocSections(docId, relativeUrl).then((resp) => { + setSections(resp); + }); + }, [docId, relativeUrl]); + + useEffect(() => { + if (project?.id) { + getDocTokenCount(project.id, docId, relativeUrl, selectedSections).then( + setTokenCount, + ); + } + }, [selectedSections, relativeUrl, docId, project?.id]); + + useEffect(() => { + if (initialSections?.length && sections.length) { + const firstSelectedSection = + initialSections?.length === 1 + ? initialSections[0] + : initialSections?.length + ? sections.find((s) => initialSections?.includes(s.point_id)) + ?.point_id + : ''; + findElementInCurrentTab( + `[data-active="true"][data-section-id="${firstSelectedSection}"]`, + )?.scrollIntoView(); + } + }, [sections.length, initialSections]); + + const handleAddToStudio = useCallback(() => { + setChosenStep({ + id: CommandBarStepEnum.ADD_TO_STUDIO, + data: { docId, relativeUrl }, + }); + setIsVisible(true); + }, [docId, relativeUrl]); + + const dropdownComponentProps = useMemo(() => { + return { + handleMoveToAnotherSide, + handleAddToStudio, + }; + }, [handleMoveToAnotherSide, handleAddToStudio]); + + useEffect(() => { + if (focusedPanel === side) { + setFocusedTabItems([ + { + label: t('Open in split view'), + Icon: SplitViewIcon, + id: 'split_view', + key: 'split_view', + onClick: handleMoveToAnotherSide, + closeOnClick: true, + shortcut: openInSplitViewShortcut, + footerHint: '', + footerBtns: [{ label: t('Move'), shortcut: ['entr'] }], + }, + ]); + } + }, [focusedPanel, side, handleMoveToAnotherSide]); + + const hasChanges = useMemo(() => { + return ( + !isDocInContext || + JSON.stringify(initialSections) !== JSON.stringify(selectedSections) + ); + }, [isDocInContext, initialSections, selectedSections]); + + const handleEditRanges = useCallback(() => { + setIsEditingSelection(true); + }, []); + + useEffect(() => { + if (studioId && !isDocInContext) { + handleEditRanges(); + } + }, [studioId, isDocInContext, handleEditRanges]); + + const handleCancelStudio = useCallback(() => { + setIsEditingSelection(false); + if (isDocInContext) { + setSelectedSections(initialSections || []); + } else { + setSelectedSections([]); + updateTabProperty( + tabKey, + 'studioId', + undefined, + side, + ); + } + }, [tabKey, side, isDocInContext, initialSections]); + + const handleSubmitToStudio = useCallback(async () => { + if (project?.id && studioId && studio) { + const patchedDoc = studio?.doc_context.find( + (f) => + f.doc_id === docId && + f.doc_source === fullDoc?.doc_source && + f.relative_url === relativeUrl, + ); + if (!patchedDoc) { + await patchCodeStudio(project.id, studioId, { + doc_context: [ + ...(studio?.doc_context || []), + { + doc_id: docId, + doc_source: fullDoc?.doc_source || '', + doc_icon: favicon || '', + doc_title: title || '', + relative_url: relativeUrl, + absolute_url: fullDoc?.absolute_url || '', + ranges: selectedSections, + hidden: false, + }, + ], + }); + } else { + patchedDoc.ranges = selectedSections; + const newContext = studio?.doc_context + .filter( + (f) => + f.doc_id !== docId || + f.doc_source !== fullDoc?.doc_source || + f.relative_url !== relativeUrl, + ) + .concat(patchedDoc); + await patchCodeStudio(project.id, studioId, { + doc_context: newContext, + }); + } + refreshCurrentProjectStudios(); + refreshStudio(); + setIsEditingSelection(false); + updateTabProperty( + tabKey, + 'isDocInContext', + true, + side, + ); + updateTabProperty( + tabKey, + 'initialSections', + selectedSections, + side, + ); + } + }, [ + project?.id, + studio, + docId, + relativeUrl, + fullDoc, + studioId, + selectedSections, + ]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, openInSplitViewShortcut)) { + handleMoveToAnotherSide(); + } else if (checkEventKeys(e, addToStudioShortcut)) { + e.preventDefault(); + e.stopPropagation(); + handleAddToStudio(); + } + }, + [handleMoveToAnotherSide, handleAddToStudio], + ); + useKeyboardNavigation( + handleKeyEvent, + focusedPanel !== side || isLeftSidebarFocused || isCommandBarVisible, + ); + + return ( +
+
+
+ {favicon ? ( + {relativeUrl} + ) : ( + + )} + {title || relativeUrl} + {!!studio && ( + <> +
+ +

+ {humanNumber(tokenCount)}{' '} + # tokens +

+ + )} +
+ {focusedPanel === side && + (studio ? ( + hasChanges || isEditingSelection ? ( +
+ {!isEditingSelection && ( + <> + +
+ + )} + + +
+ ) : ( +
+ +
+ ) + ) : ( + + + + ))} +
+
+ {sections.map((s) => { + return ( + + ); + })} +
+
+ ); +}; + +export default memo(DocTab); diff --git a/client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx b/client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx index 921eb3c276..06800abfe1 100644 --- a/client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx +++ b/client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx @@ -7,9 +7,9 @@ import { FileWithSparksIcon, StudioPlusSignIcon, } from '../../../icons'; -import { openInSplitViewShortcut } from '../../../consts/commandBar'; import { - addFileToStudioShortcut, + addToStudioShortcut, + openInSplitViewShortcut, explainFileShortcut, } from '../../../consts/shortcuts'; @@ -38,7 +38,7 @@ const ActionsDropdown = ({ } /> diff --git a/client/src/Project/CurrentTabContent/FileTab/index.tsx b/client/src/Project/CurrentTabContent/FileTab/index.tsx index 8c0576f2b6..4c6c226c6e 100644 --- a/client/src/Project/CurrentTabContent/FileTab/index.tsx +++ b/client/src/Project/CurrentTabContent/FileTab/index.tsx @@ -42,13 +42,13 @@ import { TabsContext } from '../../../context/tabsContext'; import { checkEventKeys } from '../../../utils/keyboardUtils'; import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; import { CommandBarContext } from '../../../context/commandBarContext'; -import { openInSplitViewShortcut } from '../../../consts/commandBar'; import BreadcrumbsPathContainer from '../../../components/Breadcrumbs/PathContainer'; import { RepositoriesContext } from '../../../context/repositoriesContext'; import { UIContext } from '../../../context/uiContext'; import { - addFileToStudioShortcut, + addToStudioShortcut, explainFileShortcut, + openInSplitViewShortcut, } from '../../../consts/shortcuts'; import { ProjectContext } from '../../../context/projectContext'; import Badge from '../../../components/Badge'; @@ -278,7 +278,7 @@ const FileTab = ({ const handleAddToStudio = useCallback(() => { setChosenStep({ - id: CommandBarStepEnum.ADD_FILE_TO_STUDIO, + id: CommandBarStepEnum.ADD_TO_STUDIO, data: { path, repoRef, branch }, }); setIsVisible(true); @@ -348,7 +348,7 @@ const FileTab = ({ handleExplain(); } else if (checkEventKeys(e, openInSplitViewShortcut)) { handleMoveToAnotherSide(); - } else if (checkEventKeys(e, addFileToStudioShortcut)) { + } else if (checkEventKeys(e, addToStudioShortcut)) { e.preventDefault(); e.stopPropagation(); handleAddToStudio(); @@ -387,7 +387,7 @@ const FileTab = ({ id: 'file_to_studio', key: 'file_to_studio', onClick: handleAddToStudio, - shortcut: addFileToStudioShortcut, + shortcut: addToStudioShortcut, footerHint: t('Add file to code studio context'), footerBtns: [{ label: t('Add'), shortcut: ['entr'] }], }, diff --git a/client/src/Project/CurrentTabContent/Header/TabButton.tsx b/client/src/Project/CurrentTabContent/Header/TabButton.tsx index cd61ddfdf9..0eb78c4881 100644 --- a/client/src/Project/CurrentTabContent/Header/TabButton.tsx +++ b/client/src/Project/CurrentTabContent/Header/TabButton.tsx @@ -15,7 +15,12 @@ import { import FileIcon from '../../../components/FileIcon'; import { splitPath } from '../../../utils'; import Button from '../../../components/Button'; -import { ChatBubblesIcon, CloseSignIcon, CodeStudioIcon } from '../../../icons'; +import { + ChatBubblesIcon, + CloseSignIcon, + CodeStudioIcon, + MagazineIcon, +} from '../../../icons'; import { TabsContext } from '../../../context/tabsContext'; type Props = TabType & { @@ -48,6 +53,9 @@ type Props = TabType & { repoRef: string; branch?: string | null | undefined; }; + docId?: string; + favicon?: string; + relativeUrl?: string; }; const closeTabShortcut = ['cmd', 'W']; @@ -73,6 +81,9 @@ const TabButton = ({ isFileInContext, conversationId, initialQuery, + relativeUrl, + favicon, + docId, }: Props) => { const { t } = useTranslation(); const { closeTab, setActiveLeftTab, setActiveRightTab, setFocusedPanel } = @@ -160,6 +171,9 @@ const TabButton = ({ isFileInContext, conversationId, initialQuery, + favicon, + relativeUrl, + docId, }, side, }; @@ -195,6 +209,9 @@ const TabButton = ({ isFileInContext, conversationId, initialQuery, + docId, + relativeUrl, + favicon, }); setFocusedPanel(side); }, [ @@ -238,8 +255,12 @@ const TabButton = ({ sizeClassName="w-4 h-4" className="text-brand-default" /> - ) : ( + ) : type === TabTypesEnum.STUDIO ? ( + ) : favicon ? ( + {title} + ) : ( + )}

diff --git a/client/src/components/MarkdownWithCode/FolderChip.tsx b/client/src/components/MarkdownWithCode/FolderChip.tsx index d6bdb49989..59d3e16eef 100644 --- a/client/src/components/MarkdownWithCode/FolderChip.tsx +++ b/client/src/components/MarkdownWithCode/FolderChip.tsx @@ -66,6 +66,7 @@ const FolderChip = ({ onClick, path, repoRef }: Props) => { focusedIndex={''} index={'0'} isLeftSidebarFocused={false} + isCommandBarVisible />
diff --git a/client/src/consts/commandBar.ts b/client/src/consts/commandBar.ts index 18e323b963..fac009bb1c 100644 --- a/client/src/consts/commandBar.ts +++ b/client/src/consts/commandBar.ts @@ -3,5 +3,3 @@ export const PRIVATE_REPOS = 'private_repos'; export const PUBLIC_REPOS = 'public_repos'; export const LOCAL_REPOS = 'local_repos'; export const DOCUMENTATION = 'documentation'; - -export const openInSplitViewShortcut = ['cmd', ']']; diff --git a/client/src/consts/shortcuts.ts b/client/src/consts/shortcuts.ts index b0a44a2f48..df56b1fc3a 100644 --- a/client/src/consts/shortcuts.ts +++ b/client/src/consts/shortcuts.ts @@ -2,5 +2,6 @@ export const regexToggleShortcut = ['cmd', '/']; export const newChatTabShortcut = ['option', 'N']; export const newStudioTabShortcut = ['option', 'shift', 'N']; export const explainFileShortcut = ['cmd', 'E']; -export const addFileToStudioShortcut = ['cmd', 'shift', '+']; +export const addToStudioShortcut = ['cmd', 'shift', '+']; export const useTemplateShortcut = ['cmd', 'T']; +export const openInSplitViewShortcut = ['cmd', ']']; diff --git a/client/src/context/providers/TabsContextProvider.tsx b/client/src/context/providers/TabsContextProvider.tsx index ed313f84a7..97070a07df 100644 --- a/client/src/context/providers/TabsContextProvider.tsx +++ b/client/src/context/providers/TabsContextProvider.tsx @@ -10,9 +10,10 @@ import { import { useSearchParams } from 'react-router-dom'; import { TabsContext } from '../tabsContext'; import { - StudioTabType, ChatTabType, + DocTabType, FileTabType, + StudioTabType, TabType, TabTypesEnum, } from '../../types/general'; @@ -48,6 +49,7 @@ const TabsContextProvider = ({ children }: PropsWithChildren) => { data: | Omit | Omit + | Omit | Omit, forceSide?: 'left' | 'right', ) => { @@ -103,10 +105,16 @@ const TabsContextProvider = ({ children }: PropsWithChildren) => { conversationId: data.conversationId, title: data.title, } - : { + : data.type === TabTypesEnum.STUDIO + ? { type: TabTypesEnum.STUDIO, key: data.studioId, studioId: data.studioId, + } + : { + ...data, + type: TabTypesEnum.DOC, + key: data.docId + data.relativeUrl, }; const previousTab = prev.find((t) => newTab.type === TabTypesEnum.CHAT && newTab.conversationId @@ -144,6 +152,22 @@ const TabsContextProvider = ({ children }: PropsWithChildren) => { newTabs[previousTabIndex] = t; setActiveTabAction(t); return newTabs; + } else if ( + previousTab.type === TabTypesEnum.DOC && + newTab.type === TabTypesEnum.DOC && + previousTab.studioId !== newTab.studioId + ) { + const previousTabIndex = prev.findIndex( + (t) => t.key === newTab.key, + ); + const newTabs = [...prev]; + const t = { + ...previousTab, + studioId: newTab.studioId, + }; + newTabs[previousTabIndex] = t; + setActiveTabAction(t); + return newTabs; } else { setActiveTabAction(previousTab); } @@ -204,6 +228,19 @@ const TabsContextProvider = ({ children }: PropsWithChildren) => { if (activeTab.title) { newParams.title = activeTab.title; } + } else if (activeTab.type === TabTypesEnum.DOC) { + if (activeTab.docId) { + newParams.docId = activeTab.docId; + } + if (activeTab.title) { + newParams.title = activeTab.title; + } + if (activeTab.favicon) { + newParams.favicon = activeTab.favicon; + } + if (activeTab.relativeUrl) { + newParams.relativeUrl = activeTab.relativeUrl; + } } setSearchParams(newParams, { replace: true }); } @@ -221,7 +258,8 @@ const TabsContextProvider = ({ children }: PropsWithChildren) => { const path = searchParams.get('path'); const conversationId = searchParams.get('conversationId'); const studioId = searchParams.get('studioId'); - if (!activeLeftTab && (path || conversationId || studioId)) { + const docId = searchParams.get('docId'); + if (!activeLeftTab && (path || conversationId || studioId || docId)) { const repoRef = searchParams.get('repoRef'); if (path && repoRef) { openNewTab({ @@ -238,6 +276,14 @@ const TabsContextProvider = ({ children }: PropsWithChildren) => { conversationId, title: searchParams.get('title') || undefined, }); + } else if (docId) { + openNewTab({ + type: TabTypesEnum.DOC, + docId, + title: searchParams.get('title') || undefined, + favicon: searchParams.get('favicon') || undefined, + relativeUrl: searchParams.get('relativeUrl')!, + }); } else if (studioId) { openNewTab({ type: TabTypesEnum.STUDIO, @@ -275,7 +321,10 @@ const TabsContextProvider = ({ children }: PropsWithChildren) => { }, []); const updateTabProperty = useCallback( - ( + < + T extends ChatTabType | FileTabType | StudioTabType | DocTabType, + K extends keyof T, + >( tabKey: string, objectKey: K, newValue: T[K], diff --git a/client/src/context/tabsContext.tsx b/client/src/context/tabsContext.tsx index 75b66c15fb..3e88ebec1e 100644 --- a/client/src/context/tabsContext.tsx +++ b/client/src/context/tabsContext.tsx @@ -1,6 +1,7 @@ import { createContext, Dispatch, SetStateAction } from 'react'; import { ChatTabType, + DocTabType, FileTabType, StudioTabType, TabType, @@ -11,7 +12,8 @@ type HandlersContextType = { data: | Omit | Omit - | Omit, + | Omit + | Omit, forceSide?: 'left' | 'right', ) => void; closeTab: (key: string, side: 'left' | 'right') => void; @@ -21,7 +23,7 @@ type HandlersContextType = { setLeftTabs: Dispatch>; setRightTabs: Dispatch>; updateTabProperty: < - T extends ChatTabType | FileTabType | StudioTabType, + T extends ChatTabType | FileTabType | StudioTabType | DocTabType, K extends keyof T, >( tabKey: string, diff --git a/client/src/index.css b/client/src/index.css index 382f4368f9..a55ccb9888 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1002,9 +1002,7 @@ h5, .h5 { scrollbar-width: initial; /* Firefox */ } -.readme h1, .readme h2, .readme h3, .readme h4, .readme h5, .readme h6, .readme p, .readme span, .readme div, -.doc-section h1, .doc-section h2, .doc-section h3, .doc-section h4, .doc-section h5, .doc-section h6, -.doc-section p, .doc-section span, .doc-section div { +.readme h1, .readme h2, .readme h3, .readme h4, .readme h5, .readme h6, .readme p, .readme span, .readme div { cursor: auto; } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c0e95b3153..d6b037c69d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -614,6 +614,8 @@ "Add to studio": "Add to studio", "Add file to code studio context": "Add file to code studio context", "{{count}} context files used": "{{count}} context files used", + "{{count}} context files used_one": "{{count}} context file used", + "{{count}} context files used_other": "{{count}} context files used", "Existing studio conversations": "Existing studio conversations", "Add file to studio": "Add file to studio", "Add to existing": "Add to existing", @@ -641,5 +643,12 @@ "Search templates...": "Search templates...", "Manage templates": "Manage templates", "Prompts": "Prompts", - "Manage docs": "Manage docs" -} \ No newline at end of file + "Manage docs": "Manage docs", + "Add doc to studio": "Add doc to studio", + "Whole page": "Whole page", + "Select sections": "Select sections", + "Edit sections": "Edit sections", + "Documentation in studio": "Documentation in studio", + "# selected section_one": "{{count}} selected section", + "# selected section_other": "{{count}} selected sections" +} diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 7f8d64a63e..f84d6f97cf 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -611,6 +611,8 @@ "Add to existing": "Agregar a la existencia", "Existing studio conversations": "Conversaciones de estudio existentes", "{{count}} context files used": "{{count}} archivos de contexto utilizados", + "{{count}} context files used_one": "{{count}} archivo de contexto utilizado", + "{{count}} context files used_other": "{{count}} archivos de contexto utilizados", "Add file to studio": "Agregar archivo al estudio", "Search studio conversations...": "Buscar conversaciones de estudio...", "Close all tabs": "Cierra todas las pestañas", @@ -641,5 +643,12 @@ "Manage templates": "Administrar plantillas", "Prompts": "Indicaciones", "Manage docs": "Administrar documentos", - "Paste a link to any documentation web page": "Pegue un enlace a cualquier página web de documentación" -} \ No newline at end of file + "Paste a link to any documentation web page": "Pegue un enlace a cualquier página web de documentación", + "Add doc to studio": "Agregar documento al estudio", + "Whole page": "Toda la pagina", + "Select sections": "Seleccionar secciones", + "Edit sections": "Editar secciones", + "Documentation in studio": "Documentación en estudio", + "# selected section_one": "{{count}} sección seleccionada", + "# selected section_other": "{{count}} secciónes seleccionadas" +} diff --git a/client/src/locales/it.json b/client/src/locales/it.json index 84068b70c5..9842541af7 100644 --- a/client/src/locales/it.json +++ b/client/src/locales/it.json @@ -578,6 +578,8 @@ "Add to existing": "Aggiungi a esistente", "Search studio conversations...": "Cerca conversazioni in studio ...", "{{count}} context files used": "{{count}} file di contesto utilizzati", + "{{count}} context files used_one": "{{count}} file di contesto utilizzato", + "{{count}} context files used_other": "{{count}} file di contesto utilizzati", "Add file to studio": "Aggiungi file a Studio", "Close all tabs": "Chiudi tutte le schede", "Create ranges": "Crea gamme", @@ -603,5 +605,12 @@ "Search templates...": "Modelli di ricerca ...", "Manage templates": "Gestisci modelli", "Prompts": "Richiede", - "Manage docs": "Gestisci documenti" -} \ No newline at end of file + "Manage docs": "Gestisci documenti", + "Add doc to studio": "Aggiungi doc a studio", + "Whole page": "Pagina intera", + "Select sections": "Seleziona le sezioni", + "Edit sections": "Modifica sezioni", + "Documentation in studio": "Documentazione in studio", + "# selected section_one": "{{count}} sezione selezionata", + "# selected section_other": "{{count}} sezioni selezionate" +} diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index 72ba295442..42f7af0b9d 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -599,6 +599,8 @@ "Search studio conversations...": "スタジオの会話を検索...", "Existing studio conversations": "既存のスタジオの会話", "{{count}} context files used": "{{count}}コンテキストファイルが使用されます", + "{{count}} context files used_one": "{{count}}コンテキストファイルが使用されます", + "{{count}} context files used_other": "{{count}}コンテキストファイルが使用されます", "Add file to studio": "スタジオにファイルを追加します", "Close all tabs": "すべてのタブを閉じます", "Create ranges": "範囲を作成します", @@ -622,5 +624,12 @@ "Search templates...": "テンプレートを検索...", "Manage templates": "テンプレートを管理します", "Prompts": "プロンプト", - "Manage docs": "ドキュメントを管理します" -} \ No newline at end of file + "Manage docs": "ドキュメントを管理します", + "Add doc to studio": "スタジオにドキュメントを追加します", + "Whole page": "全てのページ", + "Select sections": "セクションを選択します", + "Edit sections": "編集セクション", + "Documentation in studio": "スタジオでのドキュメント", + "# selected section_one": "選択された {{count}} つのセクション", + "# selected section_other": "選択された {{count}} つのセクション" +} diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json index 71e080a7da..ac0c42d5ec 100644 --- a/client/src/locales/zh-CN.json +++ b/client/src/locales/zh-CN.json @@ -607,6 +607,8 @@ "Add to studio": "添加到工作室", "Add file to code studio context": "将文件添加到代码工作室上下文", "{{count}} context files used": "{{count}}使用的上下文文件", + "{{count}} context files used_one": "{{count}}使用的上下文文件", + "{{count}} context files used_other": "{{count}}使用的上下文文件", "Add file to studio": "将文件添加到工作室", "Search studio conversations...": "搜索工作室对话...", "Close all tabs": "关闭所有选项卡", @@ -634,5 +636,13 @@ "Search templates...": "搜索模板...", "Manage templates": "管理模板", "Prompts": "提示", - "Manage docs": "管理文档" -} \ No newline at end of file + "Manage docs": "管理文档", + "Add doc to studio": "将DOC添加到工作室", + "Whole page": "整页", + "Select sections": "选择部分", + "Edit sections": "编辑章节", + "Documentation in studio": "Studio中的文档", + "# selected section": "{{count}} 个选定部分", + "# selected section_one": "{{count}} 个选定部分", + "# selected section_other": "{{count}} 个选定部分" +} diff --git a/client/src/types/api.ts b/client/src/types/api.ts index 2f78e01cdf..af2a170cf9 100644 --- a/client/src/types/api.ts +++ b/client/src/types/api.ts @@ -376,7 +376,7 @@ export type DocShortType = { }; export type DocPageType = { - doc_id: number; + doc_id: string; doc_source: string; relative_url: string; absolute_url: string; diff --git a/client/src/types/general.ts b/client/src/types/general.ts index def8412da7..33b4cf1f86 100644 --- a/client/src/types/general.ts +++ b/client/src/types/general.ts @@ -92,6 +92,7 @@ export enum TabTypesEnum { FILE = 'file', CHAT = 'chat', STUDIO = 'studio', + DOC = 'doc', } export type FileTabType = { @@ -128,7 +129,19 @@ export type StudioTabType = { title?: string; }; -export type TabType = FileTabType | ChatTabType | StudioTabType; +export type DocTabType = { + type: TabTypesEnum.DOC; + key: string; + docId: string; + title?: string; + favicon?: string; + relativeUrl: string; + studioId?: string; + initialSections?: string[]; + isDocInContext?: boolean; +}; + +export type TabType = FileTabType | ChatTabType | StudioTabType | DocTabType; export type DraggableTabItem = { id: string; @@ -491,9 +504,9 @@ export type CommandBarStepType = { }; export type CommandBarActiveStepType = - | AddFileToStudioStepType + | AddToStudioStepType | { - id: Exclude; + id: Exclude; data?: Record; }; @@ -503,9 +516,14 @@ export type AddFileToStudioDataType = { branch?: string | null; }; -export type AddFileToStudioStepType = { - id: CommandBarStepEnum.ADD_FILE_TO_STUDIO; - data: AddFileToStudioDataType; +export type AddDocToStudioDataType = { + docId: string; + relativeUrl: string; +}; + +export type AddToStudioStepType = { + id: CommandBarStepEnum.ADD_TO_STUDIO; + data: AddFileToStudioDataType | AddDocToStudioDataType; }; export enum CommandBarStepEnum { @@ -520,7 +538,7 @@ export enum CommandBarStepEnum { CREATE_PROJECT = 'create_project', TOGGLE_THEME = 'toggle_theme', SEARCH_FILES = 'search_files', - ADD_FILE_TO_STUDIO = 'add_file_to_studio', + ADD_TO_STUDIO = 'add_to_studio', } export enum SettingSections {