mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-05 16:46:51 +00:00
feat: agentsmd and change source
This commit is contained in:
223
AGENTS.md
Normal file
223
AGENTS.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Agent Guidelines for Helium
|
||||
|
||||
## Project Overview
|
||||
|
||||
Helium is a Nuxt 3 application for effortless WebRTC screensharing with:
|
||||
|
||||
- **Framework**: Nuxt 4 (Vue 3 + TypeScript)
|
||||
- **Package Manager**: pnpm
|
||||
- **Database**: PostgreSQL with Drizzle ORM
|
||||
- **Auth**: Clerk
|
||||
- **UI**: shadcn-vue + Tailwind CSS v4
|
||||
- **State**: Pinia stores
|
||||
- **i18n**: @nuxtjs/i18n (English & Spanish)
|
||||
|
||||
## Build/Dev/Test Commands
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm dev # Start dev server
|
||||
pnpm build # Build for production
|
||||
pnpm preview # Preview production build
|
||||
pnpm generate # Generate static site
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
```bash
|
||||
pnpm db:migrate # Generate and run migrations
|
||||
```
|
||||
|
||||
### UI Components
|
||||
|
||||
```bash
|
||||
pnpm ui:add [component] # Add shadcn-vue component
|
||||
```
|
||||
|
||||
### Running Single Tests
|
||||
|
||||
Currently no test framework is configured. If adding tests, recommend Vitest for unit tests and Playwright for e2e.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── components/ # Vue components
|
||||
│ ├── app/ # Application-specific components
|
||||
│ └── ui/ # shadcn-vue UI components
|
||||
├── composables/ # Vue composables (e.g., useWebSocketUrl)
|
||||
├── layouts/ # Nuxt layouts
|
||||
├── lib/ # Utilities, DB schema, types
|
||||
│ ├── db/ # Database schema and utils
|
||||
│ ├── schema/ # Zod validation schemas
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ └── utils/ # Helper functions
|
||||
├── middleware/ # Route middleware (e.g., auth)
|
||||
├── pages/ # File-based routing
|
||||
├── plugins/ # Nuxt plugins
|
||||
└── state/ # Pinia stores
|
||||
|
||||
server/
|
||||
├── api/ # API endpoints
|
||||
├── cron/ # Scheduled tasks
|
||||
└── routes/ # Server routes (e.g., WebSocket)
|
||||
|
||||
drizzle/ # Database migrations
|
||||
i18n/ # Translation files
|
||||
public/ # Static assets
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### TypeScript
|
||||
|
||||
- **Always use TypeScript**: No `.js` files. All code must be typed.
|
||||
- **Type imports**: Use `import type` for type-only imports
|
||||
|
||||
```typescript
|
||||
import type { PrimitiveProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
```
|
||||
|
||||
- **Explicit return types**: Prefer explicit return types for functions
|
||||
- **No `any`**: Avoid `any` type. Use `unknown` or proper typing
|
||||
- **Interface vs Type**: Use `interface` for object shapes, `type` for unions/intersections
|
||||
|
||||
### Imports
|
||||
|
||||
- **Path aliases**: Use `@/` for app directory imports and `~/` for root imports
|
||||
|
||||
```typescript
|
||||
import { cn } from "@/lib/utils";
|
||||
import { schema } from "~/lib/schema/new-preset";
|
||||
```
|
||||
|
||||
- **Import order**:
|
||||
1. External packages
|
||||
2. Vue/Nuxt imports
|
||||
3. Type imports
|
||||
4. Internal utilities
|
||||
5. Components
|
||||
6. Relative imports
|
||||
|
||||
### Vue Components
|
||||
|
||||
- **Script Setup**: Always use `<script setup lang="ts">`
|
||||
- **Props**: Define with TypeScript interfaces
|
||||
|
||||
```typescript
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants["variant"];
|
||||
size?: ButtonVariants["size"];
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: "button",
|
||||
});
|
||||
```
|
||||
|
||||
- **Template**: Keep templates clean, extract complex logic to composables
|
||||
- **Component naming**: PascalCase for components, kebab-case for files in multi-word components
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Files**:
|
||||
- Components: PascalCase (e.g., `Button.vue`, `EditPresetDialog.vue`)
|
||||
- Composables: camelCase starting with `use` (e.g., `useWebSocketUrl.ts`)
|
||||
- API routes: kebab-case with HTTP method (e.g., `[id].get.ts`, `[id].delete.ts`)
|
||||
- Database: camelCase for tables (e.g., `schema.ts`, `migrate.ts`)
|
||||
- **Variables**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE for true constants, camelCase for config objects
|
||||
- **Functions**: camelCase, descriptive verbs (e.g., `createPreset`, `getPresetById`)
|
||||
- **Components**: PascalCase
|
||||
|
||||
### Database (Drizzle)
|
||||
|
||||
- **Schema location**: `app/lib/db/schema.ts`
|
||||
- **Relations**: Define relations separately after table definitions
|
||||
- **Column naming**: camelCase in TypeScript, snake_case in SQL
|
||||
|
||||
```typescript
|
||||
export const presets = pgTable("presets", {
|
||||
createdBy: text("created_by").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
- **Migrations**: Always generate migrations with `pnpm db:migrate`
|
||||
|
||||
### API Routes (Server)
|
||||
|
||||
- **Event handlers**: Use `defineEventHandler`
|
||||
- **Auth**: Check `event.context.auth()` for authentication
|
||||
|
||||
```typescript
|
||||
const { isAuthenticated, userId } = event.context.auth();
|
||||
if (!isAuthenticated || !userId) {
|
||||
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
||||
}
|
||||
```
|
||||
|
||||
- **Error handling**: Use `createError` for HTTP errors
|
||||
- **Validation**: Use Zod schemas from `app/lib/schema/`
|
||||
- **Response format**: Consistent structure with `success` and `data`/`message` fields
|
||||
|
||||
```typescript
|
||||
return {
|
||||
success: true,
|
||||
data: preset,
|
||||
};
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Server**: Use `createError()` with proper status codes
|
||||
- **Client**: Use `toast` from `vue-sonner` for user feedback
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await $fetch("/api/presets", { method: "POST" });
|
||||
toast.success(t("presetCreatedSuccessfully"));
|
||||
} catch (error) {
|
||||
toast.error(t("failedToCreatePreset"));
|
||||
}
|
||||
```
|
||||
|
||||
- **Validation**: Use Zod with `.safeParse()` and handle errors
|
||||
- **Always catch promises**: Never leave promises unhandled
|
||||
|
||||
### Formatting
|
||||
|
||||
- **Indentation**: 2 spaces
|
||||
- **Quotes**: Double quotes for strings
|
||||
- **Semicolons**: Optional but consistent within files
|
||||
- **Line length**: Aim for 80-100 characters, break at 120
|
||||
- **Trailing commas**: Use in multiline objects/arrays
|
||||
- **Comments**: Prevent comments where possible, if the code is not understandable by itself.
|
||||
|
||||
### State Management
|
||||
|
||||
- **Pinia stores**: Located in `app/state/`
|
||||
- **Store naming**: Descriptive (e.g., `streamer.ts`, `viewer.ts`)
|
||||
- **Composables**: Prefer composables over stores for simple state
|
||||
|
||||
### i18n
|
||||
|
||||
- **Always use**: Use `useI18n()` composable for all user-facing strings. That is, you must localize any user-facing text.
|
||||
|
||||
```typescript
|
||||
const { t } = useI18n();
|
||||
<h1>{{ t('presets') }}</h1>
|
||||
```
|
||||
|
||||
- **Keys**: camelCase (e.g., `createNewPreset`, `deletePresetConfirm`)
|
||||
- **Files**: `i18n/locales/en.json` and `i18n/locales/es.json`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Authentication**: All protected routes must use auth middleware
|
||||
- **WebSocket**: Available at `/ws/signaling` for real-time communication
|
||||
- **Environment**: Use `.env` file (not committed) for DATABASE_URL and Clerk keys
|
||||
- **Clerk**: Auth provider - use `useAuth()` and `useUser()` composables
|
||||
- **Styling**: Use Tailwind utility classes, `cn()` helper for conditional classes
|
||||
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||
<div class="flex space-x-4 items-center">
|
||||
<Button @click="startScreenShare"> {{ $t('screenshare') }} </Button>
|
||||
<Button v-if="!localStream" @click="startScreenShare">
|
||||
{{ $t("screenshare") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="localStream"
|
||||
@click="changeScreenShareSource"
|
||||
variant="outline"
|
||||
>
|
||||
{{ $t("changeSource") }}
|
||||
</Button>
|
||||
<PresetSelect />
|
||||
</div>
|
||||
<p v-if="streamerStore.code" class="font-mono">{{ streamerStore.code }}</p>
|
||||
@@ -122,7 +131,6 @@ async function startScreenShare() {
|
||||
videofeedRef.value.srcObject = stream;
|
||||
}
|
||||
|
||||
// Detect when user stops sharing via browser UI
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.onended = () => {
|
||||
console.log("Screen sharing stopped by user");
|
||||
@@ -137,13 +145,51 @@ async function startScreenShare() {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to start screen share:", error);
|
||||
// User cancelled or permission denied
|
||||
cleanupStreaming();
|
||||
}
|
||||
}
|
||||
|
||||
async function changeScreenShareSource() {
|
||||
try {
|
||||
const newStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
audio: false,
|
||||
});
|
||||
|
||||
if (!localStream.value) return;
|
||||
|
||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||
|
||||
newVideoTrack.onended = () => {
|
||||
console.log("Screen sharing stopped by user");
|
||||
cleanupStreaming();
|
||||
};
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
const senders = pc.getSenders();
|
||||
const videoSender = senders.find(
|
||||
(sender) => sender.track?.kind === "video",
|
||||
);
|
||||
if (videoSender) {
|
||||
videoSender.replaceTrack(newVideoTrack);
|
||||
}
|
||||
});
|
||||
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
|
||||
localStream.value = newStream;
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = newStream;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to change screen share source:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupStreaming() {
|
||||
// Stop all media tracks
|
||||
if (localStream.value) {
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
@@ -151,30 +197,24 @@ function cleanupStreaming() {
|
||||
localStream.value = null;
|
||||
}
|
||||
|
||||
// Close all peer connections
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
pc.close();
|
||||
});
|
||||
|
||||
// Clear peer connections from store
|
||||
streamerStore.clearPeerConnections();
|
||||
|
||||
// Clear video element
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = null;
|
||||
}
|
||||
|
||||
// Clear room code
|
||||
streamerStore.setCode("");
|
||||
}
|
||||
|
||||
// Cleanup on component unmount
|
||||
onBeforeUnmount(() => {
|
||||
cleanupStreaming();
|
||||
closeWebSocket();
|
||||
});
|
||||
|
||||
// Cleanup on window/tab close
|
||||
onMounted(() => {
|
||||
const handleBeforeUnload = () => {
|
||||
cleanupStreaming();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"enterAnotherCode": "Enter another code",
|
||||
"disconnect": "Disconnect",
|
||||
"fullscreen": "Fullscreen",
|
||||
"screenshare": "screenshare",
|
||||
"screenshare": "Screenshare",
|
||||
"loadingPresets": "Loading presets...",
|
||||
"selectAPreset": "Select a preset",
|
||||
"noPresetsAvailable": "No presets available",
|
||||
@@ -53,5 +53,6 @@
|
||||
"presetNotFound": "Preset not found.",
|
||||
"presetCannotBeShared": "This preset cannot be shared",
|
||||
"presetSharingDisabled": "The preset owner has not enabled sharing for this preset.",
|
||||
"goBack": "Go Back"
|
||||
"goBack": "Go Back",
|
||||
"changeSource": "Change Source"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"enterAnotherCode": "introduce otro código",
|
||||
"disconnect": "Desconectar",
|
||||
"fullscreen": "Pantalla completa",
|
||||
"screenshare": "compartir pantalla",
|
||||
"screenshare": "Compartir pantalla",
|
||||
"loadingPresets": "Cargando ajustes...",
|
||||
"selectAPreset": "Seleccionar un ajuste",
|
||||
"noPresetsAvailable": "No hay ajustes disponibles",
|
||||
@@ -53,5 +53,6 @@
|
||||
"presetNotFound": "Ajuste no encontrado.",
|
||||
"presetCannotBeShared": "Este ajuste no se puede compartir",
|
||||
"presetSharingDisabled": "El propietario del ajuste no ha habilitado el uso compartido para este ajuste.",
|
||||
"goBack": "Volver"
|
||||
"goBack": "Volver",
|
||||
"changeSource": "Cambiar fuente"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user