| author | |
| committer | |
| log | 0936fd271f866f5f58cce067865c8d0262b9ea20 |
| tree | 4c18ac1a60d07b1d2dbdf659bf94396dd625d513 |
| parent | fa69f05b69d05003a6994194c12edd1d8aee080c |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Rhiz3K <rhiz3k@protonmail.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>3 files changed, 80 insertions(+), 6 deletions(-)
apps/web/src/assets/assetUrls.ts+22-2| ... | ... | @@ -9,7 +9,15 @@ import { usePreparedConnection } from "~/state/session"; |
| 9 | 9 | |
| 10 | 10 | export { resolveAssetUrl } from "@t3tools/client-runtime/state/assets"; |
| 11 | 11 | |
| 12 | export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResource): string | null { | |
| 12 | export type AssetUrlState = | |
| 13 | | { readonly _tag: "Loading" } | |
| 14 | | { readonly _tag: "Failure" } | |
| 15 | | { readonly _tag: "Success"; readonly url: string }; | |
| 16 | ||
| 17 | export function useAssetUrlState( | |
| 18 | environmentId: EnvironmentId, | |
| 19 | resource: AssetResource, | |
| 20 | ): AssetUrlState { | |
| 13 | 21 | const preparedConnection = usePreparedConnection(environmentId); |
| 14 | 22 | const result = useAtomValue( |
| 15 | 23 | assetEnvironment.createUrl({ |
| ... | ... | @@ -17,10 +25,22 @@ export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResourc |
| 17 | 25 | input: { resource }, |
| 18 | 26 | }), |
| 19 | 27 | ); |
| 28 | if (result._tag === "Failure") { | |
| 29 | return { _tag: "Failure" }; | |
| 30 | } | |
| 20 | 31 | if (preparedConnection._tag === "None" || result._tag !== "Success") { |
| 32 | return { _tag: "Loading" }; | |
| 33 | } | |
| 34 | const url = resolveAssetUrl(preparedConnection.value.httpBaseUrl, result.value.relativeUrl); | |
| 35 | return url === null ? { _tag: "Failure" } : { _tag: "Success", url }; | |
| 36 | } | |
| 37 | ||
| 38 | export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResource): string | null { | |
| 39 | const result = useAssetUrlState(environmentId, resource); | |
| 40 | if (result._tag !== "Success") { | |
| 21 | 41 | return null; |
| 22 | 42 | } |
| 23 | return resolveAssetUrl(preparedConnection.value.httpBaseUrl, result.value.relativeUrl); | |
| 43 | return result.url; | |
| 24 | 44 | } |
| 25 | 45 | |
| 26 | 46 | export function useAssetUrls( |
apps/web/src/components/files/FilePreviewPanel.tsx+50-2| ... | ... | @@ -4,6 +4,7 @@ import type { |
| 4 | 4 | ResolvedKeybindingsConfig, |
| 5 | 5 | ScopedThreadRef, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; | |
| 7 | 8 | import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; |
| 8 | 9 | import { Editor } from "@pierre/diffs/editor"; |
| 9 | 10 | import { EditorProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; |
| ... | ... | @@ -16,6 +17,7 @@ import * as Schema from "effect/Schema"; |
| 16 | 17 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 17 | 18 | |
| 18 | 19 | import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; |
| 20 | import { useAssetUrlState } from "~/assets/assetUrls"; | |
| 19 | 21 | import ChatMarkdown from "~/components/ChatMarkdown"; |
| 20 | 22 | import { OpenInPicker } from "~/components/chat/OpenInPicker"; |
| 21 | 23 | import { useClientSettings } from "~/hooks/useSettings"; |
| ... | ... | @@ -113,6 +115,43 @@ const FILE_LINK_REVEAL_UNSAFE_CSS = ` |
| 113 | 115 | `; |
| 114 | 116 | type FilePostRender = NonNullable<FileOptions<unknown>["onPostRender"]>; |
| 115 | 117 | |
| 118 | function WorkspaceImagePreview(props: { | |
| 119 | readonly environmentId: EnvironmentId; | |
| 120 | readonly threadRef: ScopedThreadRef; | |
| 121 | readonly absolutePath: string; | |
| 122 | readonly alt: string; | |
| 123 | }) { | |
| 124 | const assetUrl = useAssetUrlState(props.environmentId, { | |
| 125 | _tag: "workspace-file", | |
| 126 | threadId: props.threadRef.threadId, | |
| 127 | path: props.absolutePath, | |
| 128 | }); | |
| 129 | const [failedUrl, setFailedUrl] = useState<string | null>(null); | |
| 130 | ||
| 131 | if (assetUrl._tag === "Failure" || (assetUrl._tag === "Success" && failedUrl === assetUrl.url)) { | |
| 132 | return ( | |
| 133 | <div className="flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs leading-relaxed text-destructive"> | |
| 134 | Unable to load workspace image. | |
| 135 | </div> | |
| 136 | ); | |
| 137 | } | |
| 138 | ||
| 139 | return assetUrl._tag === "Success" ? ( | |
| 140 | <div className="flex min-h-0 flex-1 items-center justify-center overflow-auto p-4"> | |
| 141 | <img | |
| 142 | className="max-h-full max-w-full object-contain" | |
| 143 | src={assetUrl.url} | |
| 144 | alt={props.alt} | |
| 145 | onError={() => setFailedUrl(assetUrl.url)} | |
| 146 | /> | |
| 147 | </div> | |
| 148 | ) : ( | |
| 149 | <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground"> | |
| 150 | <LoaderCircle className="size-5 animate-spin" /> | |
| 151 | </div> | |
| 152 | ); | |
| 153 | } | |
| 154 | ||
| 116 | 155 | function clampFileLine(contents: string, requestedLine: number): number { |
| 117 | 156 | let lineCount = 1; |
| 118 | 157 | for (let index = 0; index < contents.length; index += 1) { |
| ... | ... | @@ -630,7 +669,8 @@ export default function FilePreviewPanel({ |
| 630 | 669 | const openPreview = useAtomCommand(previewEnvironment.open, { |
| 631 | 670 | reportFailure: false, |
| 632 | 671 | }); |
| 633 | const file = useProjectFileQuery(environmentId, cwd, relativePath); | |
| 672 | const isImage = relativePath !== null && isWorkspaceImagePreviewPath(relativePath); | |
| 673 | const file = useProjectFileQuery(environmentId, cwd, relativePath, !isImage); | |
| 634 | 674 | const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); |
| 635 | 675 | const [markdownView, setMarkdownView] = useState<{ |
| 636 | 676 | path: string | null; |
| ... | ... | @@ -818,7 +858,15 @@ export default function FilePreviewPanel({ |
| 818 | 858 | relativePath ? "flex" : "hidden", |
| 819 | 859 | )} |
| 820 | 860 | > |
| 821 | {relativePath && file.error && file.data === null ? ( | |
| 861 | {relativePath && isImage && absolutePath ? ( | |
| 862 | <WorkspaceImagePreview | |
| 863 | key={absolutePath} | |
| 864 | environmentId={environmentId} | |
| 865 | threadRef={threadRef} | |
| 866 | absolutePath={absolutePath} | |
| 867 | alt={relativePath} | |
| 868 | /> | |
| 869 | ) : relativePath && file.error && file.data === null ? ( | |
| 822 | 870 | <div className="flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs leading-relaxed text-destructive"> |
| 823 | 871 | {file.error} |
| 824 | 872 | </div> |
apps/web/src/components/files/projectFilesQueryState.ts+8-2| ... | ... | @@ -6,7 +6,7 @@ import type { |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | import * as Cause from "effect/Cause"; |
| 8 | 8 | import * as Option from "effect/Option"; |
| 9 | import { AsyncResult } from "effect/unstable/reactivity"; | |
| 9 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; | |
| 10 | 10 | import { useCallback } from "react"; |
| 11 | 11 | |
| 12 | 12 | import { appAtomRegistry } from "~/rpc/atomRegistry"; |
| ... | ... | @@ -14,6 +14,9 @@ import { projectEnvironment } from "~/state/projects"; |
| 14 | 14 | import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; |
| 15 | 15 | |
| 16 | 16 | const EMPTY_PROJECT_FILE_PATH = ""; |
| 17 | const EMPTY_PROJECT_FILE_QUERY_ATOM = Atom.make( | |
| 18 | AsyncResult.initial<ProjectReadFileResult, never>(false), | |
| 19 | ).pipe(Atom.withLabel("project-file-query:empty")); | |
| 17 | 20 | function optimisticFileAtom(environmentId: EnvironmentId, cwd: string, relativePath: string) { |
| 18 | 21 | return projectEnvironment.optimisticFile({ environmentId, cwd, relativePath }); |
| 19 | 22 | } |
| ... | ... | @@ -137,8 +140,11 @@ export function useProjectFileQuery( |
| 137 | 140 | environmentId: EnvironmentId, |
| 138 | 141 | cwd: string, |
| 139 | 142 | relativePath: string | null, |
| 143 | enabled = true, | |
| 140 | 144 | ): ProjectQueryState<ProjectReadFileResult> { |
| 141 | const atom = getProjectFileQueryAtom(environmentId, cwd, relativePath); | |
| 145 | const atom = enabled | |
| 146 | ? getProjectFileQueryAtom(environmentId, cwd, relativePath) | |
| 147 | : EMPTY_PROJECT_FILE_QUERY_ATOM; | |
| 142 | 148 | const result = useAtomValue(atom); |
| 143 | 149 | const refreshAtom = useAtomRefresh(atom); |
| 144 | 150 | const refresh = useCallback(() => refreshAtom(), [refreshAtom]); |