Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/ai/chat.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { type ChatMessage, create as saveChatMessage, type Usage } from "@src/db/chat_messages"
import { getChatModel } from "@src/lib/ai"
import { ulid } from "@src/lib/ids"
import { logger } from "@src/lib/logger"
import type { ComputedCharacter } from "@src/services/computeCharacter"
import type { ComputedChat } from "@src/services/computeChat"
import { executeTool } from "@src/services/toolExecution"
import { TOOL_DEFINITIONS, TOOLS } from "@src/tools"
import { type LanguageModel, streamText } from "ai"
import type { SQL } from "bun"
import { ulid } from "ulid"
import { buildSystemPrompt } from "./prompts"

export interface ChatResponse {
Expand Down
2 changes: 1 addition & 1 deletion src/ai/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Background: ${character.background || "none"}

# Your approach:

You can answer questions, provide advice, and help with rules clarifications, but your main job is to update the character sheet based on what the player tells you. You let the players focus on the game while you handle the bookkeeping. If they ask for advice, you give it, but always steer them back to the task of keeping their sheet accurate.
Your main job is to update the character sheet based on what the player tells you. The players focus on the game while you handle the bookkeeping. You are also an expert in the rules of DnD, and you can answer questions, provide advice, and help with rules clarifications. Players can ask you for advice on character optimization, spell selection, and strategy -- keep your advice concise and curt. You're here to help them, not to play the game for them.

If players ask you questions unrelated to DnD or character sheets, curtly redirect them back to your purpose. You don't want them wasting your time -- you still have a lot of character sheets to manage today!

Expand Down
30 changes: 8 additions & 22 deletions src/components/AbilitiesEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Abilities, type AbilityType } from "@src/lib/dnd"
import type { ComputedCharacter } from "@src/services/computeCharacter"
import clsx from "clsx"
import { ModalContent } from "./ui/ModalContent"
import { ModalForm, ModalFormSubmit } from "./ui/ModalForm"

export interface AbilitiesEditFormProps {
character: ComputedCharacter
Expand Down Expand Up @@ -129,16 +130,7 @@ const AbilityEditBox = ({ ability, character, values, errors }: AbilityEditBoxPr
export const AbilitiesEditForm = ({ character, values, errors = {} }: AbilitiesEditFormProps) => {
return (
<ModalContent title="Edit Abilities">
<form
id="abilities-edit-form"
hx-post={`/characters/${character.id}/edit/abilities`}
hx-vals='{"is_check": "true"}'
hx-trigger="change"
hx-target="#editModalContent"
hx-swap="morph:innerHTML"
class="needs-validation"
novalidate
>
<ModalForm id="abilities-edit-form" endpoint={`/characters/${character.id}/edit/abilities`}>
<div class="modal-body">
<div class="row row-cols-3 g-3 mb-3">
{Abilities.map((ability) => (
Expand All @@ -162,8 +154,9 @@ export const AbilitiesEditForm = ({ character, values, errors = {} }: AbilitiesE
name="note"
rows={2}
placeholder="Add a note about these ability changes..."
value={values.note ?? ""}
/>
>
{values.note ?? ""}
</textarea>
</div>

{/* General Errors */}
Expand All @@ -177,18 +170,11 @@ export const AbilitiesEditForm = ({ character, values, errors = {} }: AbilitiesE
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
hx-post={`/characters/${character.id}/edit/abilities`}
hx-vals='{"is_check": "false"}'
hx-target="#editModalContent"
hx-swap="morph:innerHTML"
>
<ModalFormSubmit endpoint={`/characters/${character.id}/edit/abilities`}>
Update Abilities
</button>
</ModalFormSubmit>
</div>
</form>
</ModalForm>
</ModalContent>
)
}
8 changes: 4 additions & 4 deletions src/components/AvatarCropper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ export const AvatarCropper = ({ character, avatarIndex }: AvatarCropperProps) =>
movable
resizable
zoomable
data-existingx={existingCrop.x || undefined}
data-existingy={existingCrop.y || undefined}
data-existingw={existingCrop.width || undefined}
data-existingh={existingCrop.height || undefined}
data-existingx={existingCrop.x ?? null}
data-existingy={existingCrop.y ?? null}
data-existingw={existingCrop.width ?? null}
data-existingh={existingCrop.height ?? null}
Comment on lines +41 to +44
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data-existing* attributes are set to null when values are not present, but JavaScript's dataset API converts null to the string "null". This means the condition existingx != null on line 136 of avatar-cropper.js will always be true (even when attributes are "null"), breaking the logic.

Change ?? null to ?? undefined for all four data attributes, as undefined won't render the attribute at all, which is the correct behavior.

Suggested change
data-existingx={existingCrop.x ?? null}
data-existingy={existingCrop.y ?? null}
data-existingw={existingCrop.width ?? null}
data-existingh={existingCrop.height ?? null}
data-existingx={existingCrop.x ?? undefined}
data-existingy={existingCrop.y ?? undefined}
data-existingw={existingCrop.width ?? undefined}
data-existingh={existingCrop.height ?? undefined}

Copilot uses AI. Check for mistakes.
>
<cropper-grid role="grid" covered></cropper-grid>
<cropper-crosshair centered></cropper-crosshair>
Expand Down
19 changes: 16 additions & 3 deletions src/components/AvatarDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import type { CropPercents } from "@src/db/character_avatars"
import type { CharacterAvatarWithUrl, ComputedCharacter } from "@src/services/computeCharacter"
import type { CharacterAvatarWithUrl } from "@src/services/computeCharacter"

// Minimal avatar type - only the fields actually used by this component
type MinimalAvatar = Omit<
CharacterAvatarWithUrl,
"id" | "character_id" | "upload_id" | "created_at" | "updated_at"
>

// Minimal character type needed for avatar display
interface CharacterWithAvatars {
id: string
name: string
avatars: MinimalAvatar[]
}

export interface AvatarDisplayProps {
character: ComputedCharacter
character: CharacterWithAvatars
avatarIndex?: number
mode: "clickable-gallery" | "clickable-lightbox" | "display-only"
className?: string
Expand Down Expand Up @@ -65,7 +78,7 @@ export const AvatarDisplay = ({
}: AvatarDisplayProps) => {
// For clickable-gallery mode, use primary avatar
// For other modes, use specified index or primary
let avatar: CharacterAvatarWithUrl | undefined
let avatar: MinimalAvatar | undefined
if (mode === "clickable-gallery") {
avatar = character.avatars.find((a) => a.is_primary) || character.avatars[0]
} else {
Expand Down
25 changes: 19 additions & 6 deletions src/components/AvatarLightbox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ComputedCharacter } from "@src/services/computeCharacter"
import { AvatarDisplay } from "./AvatarDisplay"
import { ModalContent } from "./ui/ModalContent"

export interface AvatarLightboxProps {
Expand All @@ -12,15 +11,29 @@ export const AvatarLightbox = ({ character, currentIndex }: AvatarLightboxProps)
const prevIndex = currentIndex > 0 ? currentIndex - 1 : totalAvatars - 1
const nextIndex = currentIndex < totalAvatars - 1 ? currentIndex + 1 : 0
const showNavigation = totalAvatars > 1
const currentAvatar = character.avatars[currentIndex]

if (!currentAvatar) {
return (
<ModalContent title="Avatar Not Found">
<div class="modal-body">
<p>Avatar not found.</p>
</div>
</ModalContent>
)
}

return (
<ModalContent title={`Avatar ${currentIndex + 1} of ${totalAvatars}`}>
<div class="modal-body position-relative" style="min-height: 400px;">
{/* Main avatar display */}
<div class="d-flex justify-content-center align-items-center">
<div style="max-width: 600px; width: 100%;">
<AvatarDisplay character={character} avatarIndex={currentIndex} mode="display-only" />
</div>
{/* Main avatar display - show full uncropped image */}
<div class="d-flex justify-content-center align-items-center" style="min-height: 400px;">
<img
src={currentAvatar.uploadUrl}
alt={`${character.name} avatar ${currentIndex + 1}`}
class="img-fluid"
style="max-height: 600px; object-fit: contain;"
/>
</div>

{/* Navigation arrows */}
Expand Down
38 changes: 12 additions & 26 deletions src/components/CastSpellForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SpellDetail } from "@src/components/SpellDetail"
import { spells } from "@src/lib/dnd/spells"
import type { ComputedCharacter } from "@src/services/computeCharacter"
import { ModalContent } from "./ui/ModalContent"
import { ModalForm, ModalFormSubmit } from "./ui/ModalForm"
import { Select } from "./ui/Select"

export interface CastSpellFormProps {
Expand Down Expand Up @@ -84,16 +85,7 @@ export const CastSpellForm = ({ character, values = {}, errors = {} }: CastSpell
{/* Spell Details */}
<SpellDetail spell={spell} compact={true} class="mb-3" />

<form
id="cast-spell-form"
hx-post={`/characters/${character.id}/castspell`}
hx-vals='{"is_check": "true"}'
hx-trigger="change"
hx-target="#editModalContent"
hx-swap="morph:innerHTML"
class="needs-validation"
novalidate
>
<ModalForm id="cast-spell-form" endpoint={`/characters/${character.id}/castspell`}>
{/* Hidden fields */}
<input type="hidden" name="spell_id" value={values.spell_id} />

Expand All @@ -113,11 +105,11 @@ export const CastSpellForm = ({ character, values = {}, errors = {} }: CastSpell
class="form-check-input"
type="checkbox"
name="as_ritual"
id="as_ritual"
id="as-ritual"
value="true"
checked={asRitual}
/>
<label class="form-check-label" for="as_ritual">
<label class="form-check-label" for="as-ritual">
Cast as ritual (takes +10 minutes, no spell slot consumed)
</label>
</div>
Expand All @@ -128,13 +120,13 @@ export const CastSpellForm = ({ character, values = {}, errors = {} }: CastSpell
{/* Spell Slot Selection */}
{!isCantrip && !asRitual && (
<div class="mb-3">
<label class="form-label d-block" for="slot_level">
<label class="form-label d-block" for="slot-level">
Select Spell Slot Level
</label>

<Select
name="slot_level"
id="slot_level"
id="slot-level"
options={slotOptions}
placeholder="Select a spell slot level"
required={true}
Expand All @@ -161,27 +153,21 @@ export const CastSpellForm = ({ character, values = {}, errors = {} }: CastSpell
name="note"
rows={2}
placeholder="Add a note about casting this spell..."
value={values?.note || ""}
/>
>
{values?.note || ""}
</textarea>
</div>

<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
hx-post={`/characters/${character.id}/castspell`}
hx-vals='{"is_check": "false"}'
hx-target="#editModalContent"
hx-swap="morph:innerHTML"
>
<ModalFormSubmit endpoint={`/characters/${character.id}/castspell`}>
<i class="bi bi-lightning-fill me-1"></i>
Cast {spell.name}
</button>
</ModalFormSubmit>
</div>
</form>
</ModalForm>
</div>
</ModalContent>
)
Expand Down
Loading
Loading