Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
322 changes: 159 additions & 163 deletions frontend/src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
uiStateAtom,
uiThemeAtom
} from 'state'
import { CurrentUIProvider } from './CurrentUiContext'
import { useUIEngine } from 'hooks'
import ShareDialog from './ShareDialog'
import { Button } from './ui/button'

Expand Down Expand Up @@ -148,179 +148,175 @@ export default function Chat({ isShared = false }: { isShared: boolean }) {
}
}, [onDropFile])

useUIEngine()
return (
<CurrentUIProvider>
<div
ref={contentRef}
role='button'
tabIndex={0}
onDragEnter={() => setDragging(true)}
onDragExit={() => setDragging(false)}
onDrop={(e: React.DragEvent) => {
e.preventDefault()
setDragging(false)
if (!modelSupportsImages) {
console.warn('Model does not have vision capabilities')
return
}
<div
ref={contentRef}
role='button'
tabIndex={0}
onDragEnter={() => setDragging(true)}
onDragExit={() => setDragging(false)}
onDrop={(e: React.DragEvent) => {
e.preventDefault()
setDragging(false)
if (!modelSupportsImages) {
console.warn('Model does not have vision capabilities')
return
}

let file: File | null | undefined
let fileType: string | undefined
if (e.dataTransfer.items.length > 0) {
for (const it of e.dataTransfer.items) {
if (it.kind === 'file') {
file = it.getAsFile()
fileType = file?.type
if (file?.type.startsWith('image/')) break
else file = undefined
}
}
} else {
for (const f of e.dataTransfer.files) {
let file: File | null | undefined
let fileType: string | undefined
if (e.dataTransfer.items.length > 0) {
for (const it of e.dataTransfer.items) {
if (it.kind === 'file') {
file = it.getAsFile()
fileType = file?.type
if (f.type.startsWith('image/')) break
if (file?.type.startsWith('image/')) break
else file = undefined
}
}
e.dataTransfer.clearData()

if (file) {
onDropFile(file)
} else {
console.warn('File type not supported', fileType)
} else {
for (const f of e.dataTransfer.files) {
fileType = file?.type
if (f.type.startsWith('image/')) break
else file = undefined
}
}}
onDragOver={(e: React.DragEvent) => {
e.preventDefault()
}}
className={cn(
'overflow-y-none relative flex h-[calc(100vh-12em)] max-h-[calc(100vh-6em)] w-full flex-col gap-3 px-[3%] pb-4 align-middle md:px-[10%]',
sidebarState === 'closed' ? '' : 'md:px-[3%]'
)}
>
{id === 'new' ? (
<div className={sidebarState === 'history' ? 'mt-[25%]' : 'mt-[20%]'}>
{image ? (
<img
src={image.url}
alt='Screenshot'
className='mx-auto mb-4 max-h-72 max-w-72'
/>
) : undefined}
<h1 className='mb-1 flex-row text-center text-2xl font-medium text-zinc-800 dark:text-zinc-300 md:text-3xl'>
{t('Chat Header')}
</h1>
{modelSupportsImages ? (
<h2 className='mb-4 text-center text-lg font-normal text-muted-foreground md:text-xl'>
{t('Pro Tip')}
</h2>
) : undefined}
</div>
) : (
<div>
{/* DANGER, SETTING MARGIN HERE IS WONKUS */}
<div className='flex w-full items-center justify-between p-4'>
<div className='flex items-center gap-2'>
<CircleUser strokeWidth={1} className='flex w-4' />
<HoverCard>
<HoverCardTrigger className='flex'>
<div className='flex cursor-help items-center justify-start gap-2.5 rounded-full bg-muted p-2 brightness-[.95]'>
<span
className={cn(
'text-md max-w-[calc(100vw-200px)] truncate px-4 text-left font-sans font-normal leading-[20px] md:max-w-[calc(100vw-450px)]',
sidebarState !== 'closed' &&
'md:max-w-[calc(100vw-750px)]'
)}
>
{lastRender &&
uiState.prompt.startsWith("Let's make") ? (
<ImageIcon
strokeWidth={1}
className='float-left mr-2 h-4 w-4'
/>
) : undefined}
{uiState.prompt === '' ? (
<ImageIcon strokeWidth={1} className='h-4 w-4' />
) : (
uiState.prompt
)}
</span>
</div>
<span className='my-auto ml-2 hidden h-4 flex-shrink-0 rounded-sm bg-muted px-2 text-xs text-zinc-500 brightness-[.95] md:block'>
Version {item.version(versionIdx)}
</span>
</HoverCardTrigger>
<HoverCardContent className='ml-36 w-[1000px] max-w-[calc(70vw)] rounded-[20px]'>
<div className='flex'>
{(image ?? lastRender) ? (
<div className='mr-2 flex-shrink-0'>
<img
src={image?.url ?? lastRender?.url}
alt='Screenshot'
className='flex max-h-56 max-w-56'
/>
</div>
}
e.dataTransfer.clearData()

if (file) {
onDropFile(file)
} else {
console.warn('File type not supported', fileType)
}
}}
onDragOver={(e: React.DragEvent) => {
e.preventDefault()
}}
className={cn(
'overflow-y-none relative flex h-[calc(100vh-12em)] max-h-[calc(100vh-6em)] w-full flex-col gap-3 px-[3%] pb-4 align-middle md:px-[10%]',
sidebarState === 'closed' ? '' : 'md:px-[3%]'
)}
>
{id === 'new' ? (
<div className={sidebarState === 'history' ? 'mt-[25%]' : 'mt-[20%]'}>
{image ? (
<img
src={image.url}
alt='Screenshot'
className='mx-auto mb-4 max-h-72 max-w-72'
/>
) : undefined}
<h1 className='mb-1 flex-row text-center text-2xl font-medium text-zinc-800 dark:text-zinc-300 md:text-3xl'>
{t('Chat Header')}
</h1>
{modelSupportsImages ? (
<h2 className='mb-4 text-center text-lg font-normal text-muted-foreground md:text-xl'>
{t('Pro Tip')}
</h2>
) : undefined}
</div>
) : (
<div>
{/* DANGER, SETTING MARGIN HERE IS WONKUS */}
<div className='flex w-full items-center justify-between p-4'>
<div className='flex items-center gap-2'>
<CircleUser strokeWidth={1} className='flex w-4' />
<HoverCard>
<HoverCardTrigger className='flex'>
<div className='flex cursor-help items-center justify-start gap-2.5 rounded-full bg-muted p-2 brightness-[.95]'>
<span
className={cn(
'text-md max-w-[calc(100vw-200px)] truncate px-4 text-left font-sans font-normal leading-[20px] md:max-w-[calc(100vw-450px)]',
sidebarState !== 'closed' &&
'md:max-w-[calc(100vw-750px)]'
)}
>
{lastRender && uiState.prompt.startsWith("Let's make") ? (
<ImageIcon
strokeWidth={1}
className='float-left mr-2 h-4 w-4'
/>
) : undefined}
<div className='flex-grow'>
<p>{uiState.prompt}</p>
{uiState.prompt === '' ? (
<ImageIcon strokeWidth={1} className='h-4 w-4' />
) : (
uiState.prompt
)}
</span>
</div>
<span className='my-auto ml-2 hidden h-4 flex-shrink-0 rounded-sm bg-muted px-2 text-xs text-zinc-500 brightness-[.95] md:block'>
Version {item.version(versionIdx)}
</span>
</HoverCardTrigger>
<HoverCardContent className='ml-36 w-[1000px] max-w-[calc(70vw)] rounded-[20px]'>
<div className='flex'>
{(image ?? lastRender) ? (
<div className='mr-2 flex-shrink-0'>
<img
src={image?.url ?? lastRender?.url}
alt='Screenshot'
className='flex max-h-56 max-w-56'
/>
</div>
) : undefined}
<div className='flex-grow'>
<p>{uiState.prompt}</p>
</div>
</HoverCardContent>
</HoverCard>
</div>
<div className='flex justify-end'>
{!isShared && <ShareDialog />}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
className='-mr-4 justify-end hover:bg-transparent'
onClick={() => {
const [ext, mime] = mimeTypeAndExtension(framework)
// TODO: add unsplash images to the output
downloadStringAsFile(
wrappedCode(
item.pureHTML(versionIdx) ?? '<h1>Error</h1>',
framework,
theme ?? themes[0]
),
mime,
`${item.name}${ext}`
)
}}
>
<DownloadIcon />
</Button>
</TooltipTrigger>
<TooltipContent side='bottom'>
Download the HTML
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
className='-mr-4 hover:bg-transparent'
onClick={() => {
setSidebarState(
sidebarState === 'closed' ? 'versions' : 'closed'
)
}}
>
<CounterClockwiseClockIcon />
</Button>
</TooltipTrigger>
<TooltipContent side='bottom'>
Toggle version history
</TooltipContent>
</Tooltip>
</div>
</div>
</HoverCardContent>
</HoverCard>
</div>
<div className='flex justify-end'>
{!isShared && <ShareDialog />}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
className='-mr-4 justify-end hover:bg-transparent'
onClick={() => {
const [ext, mime] = mimeTypeAndExtension(framework)
// TODO: add unsplash images to the output
downloadStringAsFile(
wrappedCode(
item.pureHTML(versionIdx) ?? '<h1>Error</h1>',
framework,
theme ?? themes[0]
),
mime,
`${item.name}${ext}`
)
}}
>
<DownloadIcon />
</Button>
</TooltipTrigger>
<TooltipContent side='bottom'>Download the HTML</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
className='-mr-4 hover:bg-transparent'
onClick={() => {
setSidebarState(
sidebarState === 'closed' ? 'versions' : 'closed'
)
}}
>
<CounterClockwiseClockIcon />
</Button>
</TooltipTrigger>
<TooltipContent side='bottom'>
Toggle version history
</TooltipContent>
</Tooltip>
</div>
<HTMLAnnotator id={id} error={uiState.error} />
</div>
)}
<Prompt isEditing={isEditing} imageUploadRef={imageUploadRef} />
</div>
</CurrentUIProvider>
<HTMLAnnotator id={id} error={uiState.error} />
</div>
)}
<Prompt isEditing={isEditing} imageUploadRef={imageUploadRef} />
</div>
)
}
9 changes: 4 additions & 5 deletions frontend/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { Monaco } from '@monaco-editor/react'
import Editor, { loader } from '@monaco-editor/react'
import { useThrottle, useVersion } from 'hooks'
import { useThrottle, useVersion, useUIActions } from 'hooks'
import { useAtom, useAtomValue } from 'jotai'
import anysphere from 'lib/anysphere'
import { mimeTypeAndExtension } from 'lib/utils'
import type { Position, editor } from 'monaco-editor'
import { configureMonacoTailwindcss, tailwindcssData } from 'monaco-tailwindcss'
import type { Plugin } from 'prettier'
import prettierPluginEstree from 'prettier/plugins/estree'
import { useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'react-router-dom'
import {
ItemWrapper,
Expand All @@ -17,7 +17,6 @@ import {
useSaveHistory,
type Framework
} from 'state'
import CurrentUiContext from './CurrentUiContext'

import 'monaco-editor/esm/vs/basic-languages/css/css.contribution'
import 'monaco-editor/esm/vs/basic-languages/html/html.contribution'
Expand Down Expand Up @@ -112,7 +111,7 @@ export default function CodeEditor({
const printWidth = 200
const params = useParams()
const id = params.id ?? 'new'
const uiContext = useContext(CurrentUiContext)
const { updateState } = useUIActions()
const [readOnly, setReadOnly] = useState(framework !== 'html')
const editor = useRef<editor.IStandaloneCodeEditor>()

Expand Down Expand Up @@ -193,7 +192,7 @@ export default function CodeEditor({
clearTimeout(bufferTimer.current)
if (bufferedCode !== '') {
bufferTimer.current = setTimeout(() => {
uiContext.emit('ui-state', { editedHTML: bufferedCode })
updateState({ editedHTML: bufferedCode })
item.editChapter(bufferedCode, versionIdx)
}, 2000)
}
Expand Down
Loading