Skip to content

Commit

Permalink
Anon User Editing, other code editor/ai page improvements for empty s…
Browse files Browse the repository at this point in the history
…tates, fix code editor not being scrollable (#52)

* fix code editor scrolling

* fix editor page when not logged in

* warn on ai page if they're not signed in

* improve header for editor page

* use snippet type from url in anonymous snippets

* improved cta on ai page

* fix type issues
  • Loading branch information
seveibar authored Oct 12, 2024
1 parent 6a340a8 commit ea87e9f
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 79 deletions.
Binary file modified bun.lockb
100644 → 100755
Binary file not shown.
27 changes: 25 additions & 2 deletions src/components/AiChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Link, useLocation } from "wouter"
import { useSnippet } from "@/hooks/use-snippet"
import { Edit2 } from "lucide-react"
import { SnippetLink } from "./SnippetLink"
import { useGlobalStore } from "@/hooks/use-global-store"
import { useSignIn } from "@/hooks/use-sign-in"

export default function AIChatInterface({
code,
Expand All @@ -19,8 +21,10 @@ export default function AIChatInterface({
onStartStreaming,
onStopStreaming,
errorMessage,
disabled,
}: {
code: string
disabled?: boolean
hasUnsavedChanges: boolean
snippetId?: string | null
onCodeChange: (code: string) => void
Expand All @@ -36,6 +40,8 @@ export default function AIChatInterface({
const [currentCodeBlock, setCurrentCodeBlock] = useState<string | null>(null)
const [location, navigate] = useLocation()
const isStreamingRef = useRef(false)
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
const signIn = useSignIn()

useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
Expand Down Expand Up @@ -159,12 +165,28 @@ export default function AIChatInterface({
</Button>
</div>
)}
{messages.length === 0 && (
{messages.length === 0 && isLoggedIn && (
<div className="text-gray-500 text-xl text-center pt-[30vh] flex flex-col items-center">
<div>Submit a prompt to {snippet ? "edit!" : "get started!"}</div>
<div className="text-6xl mt-4"></div>
</div>
)}
{!isLoggedIn && (
<div className="text-gray-500 text-xl text-center pt-[30vh] flex flex-col items-center">
<div>
Sign in use the AI chat or{" "}
<Link className="text-blue-500 underline" href="/quickstart">
use the regular editor
</Link>
</div>
<div className="mt-4 flex gap-2">
<Button onClick={() => signIn()}>Sign In</Button>
<Button onClick={() => signIn()} variant="outline">
Sign Up
</Button>
</div>
</div>
)}
{messages.map((message, index) => (
<AiChatMessage key={index} message={message} />
))}
Expand All @@ -176,6 +198,7 @@ export default function AIChatInterface({
onClick={() => {
addMessage(`Fix this error: ${errorMessage}`)
}}
disabled={!isLoggedIn}
className="mb-2 bg-green-50 hover:bg-green-100"
variant="outline"
>
Expand All @@ -191,7 +214,7 @@ export default function AIChatInterface({
onSubmit={async (message: string) => {
addMessage(message)
}}
disabled={isStreaming}
disabled={isStreaming || !isLoggedIn}
/>
</div>
)
Expand Down
1 change: 1 addition & 0 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default function ChatInput({ onSubmit, disabled }: ChatInputProps) {
type="submit"
size="icon"
className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-blue-500 hover:bg-blue-600 text-white rounded-full"
disabled={disabled}
>
<ArrowUp className="h-5 w-5" />
</Button>
Expand Down
27 changes: 22 additions & 5 deletions src/components/CodeAndPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,41 @@ import { ErrorBoundary } from "react-error-boundary"
import { ErrorTabContent } from "./ErrorTabContent"
import { cn } from "@/lib/utils"
import { PreviewContent } from "./PreviewContent"
import { useGlobalStore } from "@/hooks/use-global-store"
import { useUrlParams } from "@/hooks/use-url-params"
import { getSnippetTemplate } from "@/lib/get-snippet-template"

interface Props {
snippet?: Snippet | null
}

export function CodeAndPreview({ snippet }: Props) {
const axios = useAxios()
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
const urlParams = useUrlParams()
const templateFromUrl = useMemo(
() => getSnippetTemplate(urlParams.template),
[],
)
const defaultCode = useMemo(() => {
return decodeUrlHashToText(window.location.toString()) ?? snippet?.code
return (
decodeUrlHashToText(window.location.toString()) ??
snippet?.code ??
templateFromUrl.code
)
}, [])
const [code, setCode] = useState(defaultCode ?? "")
const [dts, setDts] = useState("")
const [showPreview, setShowPreview] = useState(true)
const snippetType: "board" | "package" | "model" | "footprint" =
snippet?.snippet_type ?? (templateFromUrl.type as any)

useEffect(() => {
if (snippet?.code && !defaultCode) {
if (snippet?.code) {
setCode(snippet.code)
}
}, [snippet?.code])
}, [Boolean(snippet)])

const { toast } = useToast()

const {
Expand All @@ -50,7 +66,7 @@ export function CodeAndPreview({ snippet }: Props) {
tsxRunTriggerCount,
} = useRunTsx({
code,
type: snippet?.snippet_type,
type: snippetType,
})
const qc = useQueryClient()

Expand Down Expand Up @@ -91,7 +107,7 @@ export function CodeAndPreview({ snippet }: Props) {

const hasUnsavedChanges = snippet?.code !== code

if (!snippet) {
if (!snippet && isLoggedIn) {
return <div>Loading...</div>
}

Expand All @@ -100,6 +116,7 @@ export function CodeAndPreview({ snippet }: Props) {
<EditorNav
circuitJson={circuitJson}
snippet={snippet}
snippetType={snippetType}
code={code}
isSaving={updateSnippetMutation.isLoading}
hasUnsavedChanges={hasUnsavedChanges}
Expand Down
38 changes: 26 additions & 12 deletions src/components/EditorNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { cn } from "@/lib/utils"
import { DownloadButtonAndMenu } from "./DownloadButtonAndMenu"
import { TypeBadge } from "./TypeBadge"
import { SnippetLink } from "./SnippetLink"
import { useGlobalStore } from "@/hooks/use-global-store"

export default function EditorNav({
circuitJson,
Expand All @@ -42,31 +43,44 @@ export default function EditorNav({
onTogglePreview,
previewOpen,
onSave,
snippetType,
isSaving,
}: {
snippet?: Snippet | null
circuitJson: any
snippet: Snippet
code: string
snippetType?: string
hasUnsavedChanges: boolean
previewOpen: boolean
onTogglePreview: () => void
isSaving: boolean
onSave: () => void
}) {
const [, navigate] = useLocation()
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
return (
<nav className="flex items-center justify-between px-2 py-3 border-b border-gray-200 bg-white text-sm border-t">
<div className="flex items-center space-x-1">
<SnippetLink snippet={snippet} />
<Link href={`/${snippet.name}`}>
<Button variant="ghost" size="icon" className="h-6 w-6 ml-1">
<OpenInNewWindowIcon className="h-3 w-3 text-gray-700" />
</Button>
</Link>
{snippet && (
<>
<SnippetLink snippet={snippet} />
<Link href={`/${snippet.name}`}>
<Button variant="ghost" size="icon" className="h-6 w-6 ml-1">
<OpenInNewWindowIcon className="h-3 w-3 text-gray-700" />
</Button>
</Link>
</>
)}
{!isLoggedIn && (
<div className="bg-orange-100 text-orange-700 py-1 px-2 text-xs opacity-70">
Not logged in, can't save
</div>
)}
<Button
variant="outline"
size="sm"
className={"h-6 px-2 text-xs"}
disabled={!snippet}
onClick={onSave}
>
<Save className="mr-1 h-3 w-3" />
Expand Down Expand Up @@ -97,25 +111,25 @@ export default function EditorNav({
Saving...
</div>
)}
{hasUnsavedChanges && !isSaving && (
{hasUnsavedChanges && !isSaving && isLoggedIn && (
<div className="animate-fadeIn bg-yellow-100 text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded">
unsaved changes
</div>
)}
</div>
<div className="flex items-center space-x-1">
{snippet && <TypeBadge type={snippet.snippet_type} />}
{snippet && <TypeBadge type={snippetType ?? snippet.snippet_type} />}
<Button
variant="ghost"
size="sm"
disabled={hasUnsavedChanges || isSaving}
onClick={() => navigate(`/ai?snippet_id=${snippet.snippet_id}`)}
disabled={hasUnsavedChanges || isSaving || !snippet}
onClick={() => navigate(`/ai?snippet_id=${snippet!.snippet_id}`)}
>
<Sparkles className="mr-1 h-3 w-3" />
Edit with AI
</Button>
<DownloadButtonAndMenu
snippetUnscopedName={snippet.unscoped_name}
snippetUnscopedName={snippet?.unscoped_name}
circuitJson={circuitJson}
className="hidden md:flex"
/>
Expand Down
13 changes: 9 additions & 4 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ const HeaderButton = ({
href,
children,
className,
alsoHighlightForUrl,
}: {
href: string
children: React.ReactNode
className?: string
alsoHighlightForUrl?: string
}) => {
const [location] = useLocation()

if (location === href) {
if (location === href || location === alsoHighlightForUrl) {
return (
<Button
variant="ghost"
Expand Down Expand Up @@ -60,15 +62,17 @@ export default function Header() {
<HeaderButton href="/newest">Newest</HeaderButton>
</li>
<li>
<HeaderButton href="/quickstart">Editor</HeaderButton>
<HeaderButton href="/quickstart" alsoHighlightForUrl="/editor">
Editor
</HeaderButton>
</li>
<li>
<HeaderButton href="/ai">AI</HeaderButton>
</li>
<li>
<Link href="https://docs.tscircuit.com">
<a href="https://docs.tscircuit.com">
<Button variant="ghost">Docs</Button>
</Link>
</a>
</li>
</ul>
</nav>
Expand Down Expand Up @@ -127,6 +131,7 @@ export default function Header() {
<HeaderButton
className="w-full justify-start"
href="/quickstart"
alsoHighlightForUrl="/editor"
>
Editor
</HeaderButton>
Expand Down
17 changes: 4 additions & 13 deletions src/components/HeaderLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
import { useGlobalStore } from "@/hooks/use-global-store"
import { useAccountBalance } from "@/hooks/use-account-balance"
import { useIsUsingFakeApi } from "@/hooks/use-is-using-fake-api"
import { useSignIn } from "@/hooks/use-sign-in"

interface HeaderLoginProps {}

Expand All @@ -23,6 +24,7 @@ export const HeaderLogin: React.FC<HeaderLoginProps> = () => {
const setSession = useGlobalStore((s) => s.setSession)
const snippetsBaseApiUrl = useSnippetsBaseApiUrl()
const isUsingFakeApi = useIsUsingFakeApi()
const signIn = useSignIn()
const { data: accountBalance } = useAccountBalance()

if (!isLoggedIn) {
Expand All @@ -45,21 +47,10 @@ export const HeaderLogin: React.FC<HeaderLoginProps> = () => {
</Button>
) : (
<>
<Button
onClick={() => {
window.location.href = `${snippetsBaseApiUrl}/internal/oauth/github/authorize?next=${window.location.origin}/authorize`
}}
variant="ghost"
size="sm"
>
<Button onClick={() => signIn()} variant="ghost" size="sm">
Login
</Button>
<Button
size="sm"
onClick={() => {
window.location.href = `${snippetsBaseApiUrl}/internal/oauth/github/authorize?next=${window.location.origin}/authorize`
}}
>
<Button size="sm" onClick={() => signIn()}>
Sign Up
</Button>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/components/PreviewContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type PreviewContentProps =
const PreviewEmptyState = ({
triggerRunTsx,
}: { triggerRunTsx: () => void }) => (
<div className="flex items-center gap-3 bg-gray-200 text-center justify-center py-10">
<div className="flex items-center gap-3 bg-gray-100 text-center justify-center py-10">
No circuit json loaded
<Button className="bg-blue-600 hover:bg-blue-500" onClick={triggerRunTsx}>
Run Code
Expand Down
26 changes: 5 additions & 21 deletions src/hooks/use-current-snippet-id.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { useEffect, useRef, useState } from "react"
import { useUrlParams } from "./use-url-params"
import { useAxios } from "./use-axios"
import { defaultCodeForBlankPage } from "@/lib/defaultCodeForBlankCode"
import { useLocation, useParams } from "wouter"
import { useMutation } from "react-query"
import { useSnippetByName } from "./use-snippet-by-name"
import { blankPackageTemplate } from "@/lib/templates/blank-package-template"
import { blankFootprintTemplate } from "@/lib/templates/blank-footprint-template"
import { blankCircuitBoardTemplate } from "@/lib/templates/blank-circuit-board-template"
import { blank3dModelTemplate } from "@/lib/templates/blank-3d-model-template"
import { getSnippetTemplate } from "@/lib/get-snippet-template"
import { useGlobalStore } from "./use-global-store"

export const useCurrentSnippetId = (): string | null => {
const urlParams = useUrlParams()
const urlSnippetId = urlParams.snippet_id
const templateName = urlParams.template
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
const wouter = useParams()
const [location] = useLocation()
const axios = useAxios()
Expand All @@ -25,25 +23,10 @@ export const useCurrentSnippetId = (): string | null => {
: null,
)

const getTemplate = (template: string | undefined) => {
switch (template) {
case "blank-circuit-module":
return blankPackageTemplate
case "blank-footprint":
return blankFootprintTemplate
case "blank-circuit-board":
return blankCircuitBoardTemplate
case "blank-3d-model":
return blank3dModelTemplate
default:
return { code: defaultCodeForBlankPage, type: "board" }
}
}

const createSnippetMutation = useMutation({
mutationKey: ["createSnippet"],
mutationFn: async () => {
const template = getTemplate(templateName)
const template = getSnippetTemplate(templateName)
const {
data: { snippet },
} = await axios.post("/snippets/create", {
Expand All @@ -70,6 +53,7 @@ export const useCurrentSnippetId = (): string | null => {
if (location !== "/editor") return
if (wouter?.author && wouter?.snippetName) return
if ((window as any).AUTO_CREATED_SNIPPET) return
if (!isLoggedIn) return
;(window as any).AUTO_CREATED_SNIPPET = true
createSnippetMutation.mutate()
return () => {
Expand Down
Loading

0 comments on commit ea87e9f

Please sign in to comment.