mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: minor prompt redesign (#20045)
When I navigate to tasks, the prompt doesn’t feel prominent enough—even though it should be the primary element on the screen. To give it more focus, I made some small design changes. Here are both versions: **Before:** <img width="1624" height="970" alt="Screenshot 2025-09-30 at 12 10 58" src="https://github.com/user-attachments/assets/b5451ad5-4d32-4716-8e0f-e4596e7c92c8" /> **After:** <img width="1624" height="970" alt="Screenshot 2025-09-30 at 12 10 46" src="https://github.com/user-attachments/assets/f478e327-12c0-4f49-829f-8b094a6b613e" />
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { SelectTriggerProps } from "@radix-ui/react-select";
|
||||
import { API } from "api/api";
|
||||
import { getErrorDetail, getErrorMessage } from "api/errors";
|
||||
import { templateVersionPresets } from "api/queries/templates";
|
||||
@@ -29,15 +30,16 @@ import {
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { useAuthenticated } from "hooks/useAuthenticated";
|
||||
import { useExternalAuth } from "hooks/useExternalAuth";
|
||||
import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react";
|
||||
import { ArrowUpIcon, RedoIcon, RotateCcwIcon } from "lucide-react";
|
||||
import { AI_PROMPT_PARAMETER_NAME } from "modules/tasks/tasks";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import TextareaAutosize, {
|
||||
type TextareaAutosizeProps,
|
||||
} from "react-textarea-autosize";
|
||||
import { cn } from "utils/cn";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
const textareaPlaceholder = "Prompt your AI agent to start a task...";
|
||||
|
||||
type TaskPromptProps = {
|
||||
templates: Template[] | undefined;
|
||||
error: unknown;
|
||||
@@ -92,23 +94,14 @@ const TaskPromptLoadingError: FC<{
|
||||
|
||||
const TaskPromptSkeleton: FC = () => {
|
||||
return (
|
||||
<div className="border border-border border-solid rounded-lg p-4">
|
||||
<div className="space-y-4">
|
||||
{/* Textarea skeleton */}
|
||||
<TextareaAutosize
|
||||
disabled
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder={textareaPlaceholder}
|
||||
className={`border-0 resize-none w-full h-full bg-transparent rounded-lg outline-none flex min-h-[60px]
|
||||
text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`}
|
||||
/>
|
||||
<div className="border border-border border-solid rounded-3xl p-3 bg-surface-secondary">
|
||||
{/* Textarea skeleton */}
|
||||
<PromptTextarea disabled />
|
||||
|
||||
{/* Bottom controls skeleton */}
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Skeleton className="w-[208px] h-8" />
|
||||
<Skeleton className="w-[96px] h-8" />
|
||||
</div>
|
||||
{/* Bottom controls skeleton */}
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Skeleton className="w-[208px] h-8 rounded-full" />
|
||||
<Skeleton className="size-8 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -225,7 +218,7 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
{externalAuthError && <ErrorAlert error={externalAuthError} />}
|
||||
|
||||
<fieldset
|
||||
className="border border-border border-solid rounded-lg p-4"
|
||||
className="border border-border border-solid rounded-3xl p-3 bg-surface-secondary"
|
||||
disabled={createTaskMutation.isPending}
|
||||
>
|
||||
<label
|
||||
@@ -238,25 +231,15 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
>
|
||||
{isPromptReadOnly ? "Prompt defined by preset" : "Prompt"}
|
||||
</label>
|
||||
<TextareaAutosize
|
||||
<PromptTextarea
|
||||
required
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
value={presetAIPrompt || undefined}
|
||||
readOnly={isPromptReadOnly}
|
||||
placeholder={textareaPlaceholder}
|
||||
className={`border-0 resize-none w-full h-full bg-transparent rounded-lg outline-none flex min-h-[60px]
|
||||
text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm ${
|
||||
isPromptReadOnly ? "opacity-60 cursor-not-allowed" : ""
|
||||
}`}
|
||||
/>
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="templateID"
|
||||
className="text-xs font-medium text-content-primary"
|
||||
>
|
||||
<label htmlFor="templateID" className="sr-only">
|
||||
Template
|
||||
</label>
|
||||
<Select
|
||||
@@ -265,12 +248,9 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
defaultValue={templates[0].id}
|
||||
required
|
||||
>
|
||||
<SelectTrigger
|
||||
id="templateID"
|
||||
className="w-80 text-xs [&_svg]:size-icon-xs border-0 bg-surface-secondary h-8 px-3"
|
||||
>
|
||||
<PromptSelectTrigger id="templateID">
|
||||
<SelectValue placeholder="Select a template" />
|
||||
</SelectTrigger>
|
||||
</PromptSelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((template) => {
|
||||
return (
|
||||
@@ -285,40 +265,26 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{isLoadingPresets ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="presetID"
|
||||
className="text-xs font-medium text-content-primary"
|
||||
>
|
||||
Preset
|
||||
</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="presetID" className="sr-only">
|
||||
Preset
|
||||
</label>
|
||||
{isLoadingPresets ? (
|
||||
<Skeleton className="w-[320px] h-8" />
|
||||
</div>
|
||||
) : (
|
||||
presets &&
|
||||
presets.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="presetID"
|
||||
className="text-xs font-medium text-content-primary"
|
||||
>
|
||||
Preset
|
||||
</label>
|
||||
) : (
|
||||
presets &&
|
||||
presets.length > 0 && (
|
||||
<Select
|
||||
key={`preset-select-${selectedTemplate.active_version_id}`}
|
||||
name="presetID"
|
||||
value={selectedPresetId || undefined}
|
||||
onValueChange={setSelectedPresetId}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="presetID"
|
||||
className="w-80 text-xs [&_svg]:size-icon-xs border-0 bg-surface-secondary h-8 px-3"
|
||||
>
|
||||
<PromptSelectTrigger id="presetID">
|
||||
<SelectValue placeholder="Select a preset" />
|
||||
</SelectTrigger>
|
||||
</PromptSelectTrigger>
|
||||
<SelectContent>
|
||||
{presets.toSorted(sortByDefault).map((preset) => (
|
||||
{presets?.toSorted(sortByDefault).map((preset) => (
|
||||
<SelectItem value={preset.ID} key={preset.ID}>
|
||||
<span className="overflow-hidden text-ellipsis block">
|
||||
{preset.Name} {preset.Default && "(Default)"}
|
||||
@@ -327,9 +293,9 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -340,7 +306,12 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button size="sm" type="submit" disabled={isMissingExternalAuth}>
|
||||
<Button
|
||||
size="icon"
|
||||
type="submit"
|
||||
disabled={isMissingExternalAuth}
|
||||
className="rounded-full disabled:bg-surface-invert-primary disabled:opacity-70"
|
||||
>
|
||||
<Spinner
|
||||
loading={
|
||||
isLoadingExternalAuth ||
|
||||
@@ -348,9 +319,9 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
createTaskMutation.isPending
|
||||
}
|
||||
>
|
||||
<SendIcon />
|
||||
<ArrowUpIcon />
|
||||
</Spinner>
|
||||
Run task
|
||||
<span className="sr-only">Run task</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,6 +330,23 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PromptSelectTrigger: FC<SelectTriggerProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<SelectTrigger
|
||||
{...props}
|
||||
className={cn([
|
||||
className,
|
||||
`border-0 bg-surface-secondary text-sm text-content-primary gap-2 px-3
|
||||
[&_svg]:text-inherit cursor-pointer hover:bg-surface-quaternary rounded-full
|
||||
h-8 data-[state=open]:bg-surface-tertiary`,
|
||||
])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ExternalAuthButtonProps = {
|
||||
template: Template;
|
||||
missedExternalAuth: TemplateVersionExternalAuth[];
|
||||
@@ -379,7 +367,7 @@ const ExternalAuthButtons: FC<ExternalAuthButtonProps> = ({
|
||||
return (
|
||||
<div className="flex items-center gap-2" key={auth.id}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-surface-tertiary hover:bg-surface-quaternary rounded-full text-white"
|
||||
size="sm"
|
||||
disabled={isPollingExternalAuth || auth.authenticated}
|
||||
onClick={() => {
|
||||
@@ -446,3 +434,17 @@ async function createTaskWithLatestTemplateVersion(
|
||||
template_version_preset_id: presetId,
|
||||
});
|
||||
}
|
||||
|
||||
const PromptTextarea: FC<TextareaAutosizeProps> = (props) => {
|
||||
return (
|
||||
<TextareaAutosize
|
||||
required
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="Prompt your AI agent to start a task..."
|
||||
className={`border-0 px-3 py-2 resize-none w-full h-full bg-transparent rounded-lg
|
||||
outline-none flex min-h-24 text-sm shadow-sm text-content-primary
|
||||
placeholder:text-content-secondary md:text-sm ${props.readOnly ? "opacity-60 cursor-not-allowed" : ""}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user