Imported from hankchiutw/nitra-ai-chat (
AGENTS.md). Install upstream withnpx skills add hankchiutw/nitra-ai-chat. Copyright stays with the author.
AGENTS.md
Project Overview
Nitra AI Chat is a Vue 3 application built with the Quasar Framework, utilizing TypeScript, Composition API, and Pinia for state management. The project uses pnpm as the package manager and follows a feature-based architecture.
Technology Stack
- Framework: Vue 3 (v3.5.22) with Composition API
- UI Framework: Quasar (v2.16.0)
- State Management: Pinia (v3.0.1)
- Router: Vue Router (v4.0.12)
- Build Tool: Vite (via @quasar/app-vite)
- Language: TypeScript (v5.9.2)
- Package Manager: pnpm
- Linting: ESLint (v9.14.0) + Prettier (v3.3.3)
Project Structure
src/
├── App.vue # Root application component
├── assets/ # Static assets (images, fonts, etc.)
├── boot/ # Boot files (run before app initialization)
├── components/ # Reusable Vue components
│ ├── EssentialLink.vue
│ ├── ExampleComponent.vue
│ └── models.ts
├── css/ # Global styles
├── layouts/ # Layout components
│ └── MainLayout.vue
├── pages/ # Page components (route views)
│ ├── IndexPage.vue
│ └── ErrorNotFound.vue
├── router/ # Vue Router configuration
│ ├── index.ts
│ └── routes.ts
├── stores/ # Pinia stores
│ ├── index.ts
│ └── example-store.ts
└── env.d.ts # TypeScript environment declarations
Architecture Principles
1. Feature-Based Organization
When implementing new features, organize code by feature rather than by type. Create feature directories that contain all related components, stores, composables, and utilities.
Example structure for a chat feature:
src/
└── features/
└── chat/
├── components/
│ ├── ChatMessage.vue
│ ├── ChatInput.vue
│ └── ChatList.vue
├── stores/
│ └── useChatStore.ts
├── composables/
│ └── useChat.ts
├── types/
│ └── chat.types.ts
└── utils/
└── chatHelpers.ts
2. Composition API
All components should use the Vue 3 Composition API with <script setup> syntax:
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
</script>
3. Pinia State Management
Use Pinia for global state management. Each store should be self-contained and feature-specific.
Store conventions:
- Use
defineStorewith the Composition API style or Options API style - Enable HMR (Hot Module Replacement) for development
- Keep stores focused on a single feature or domain
- Use TypeScript for type safety
Example store (Composition API style - recommended):
import { defineStore, acceptHMRUpdate } from 'pinia';
import { ref, computed } from 'vue';
export const useFeatureStore = defineStore('feature', () => {
// State (using ref)
const data = ref<DataType[]>([]);
// Getters (using computed)
const filteredData = computed(() => {
return data.value.filter(/* ... */);
});
// Actions (using functions)
function addData(item: DataType) {
data.value.push(item);
}
return {
data,
filteredData,
addData,
};
});
// Enable HMR for development
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useFeatureStore, import.meta.hot));
}
Example store (Options API style):
import { defineStore, acceptHMRUpdate } from 'pinia';
export const useFeatureStore = defineStore('feature', {
state: () => ({
data: [] as DataType[],
}),
getters: {
filteredData: (state) => {
return state.data.filter(/* ... */);
},
},
actions: {
addData(item: DataType) {
this.data.push(item);
},
},
});
// Enable HMR for development
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useFeatureStore, import.meta.hot));
}
4. Component Structure
Components should follow this structure:
<script setup lang="ts">
// 1. Imports
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
// 2. Props & Emits
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
});
const emit = defineEmits<{
update: [value: number];
close: [];
}>();
// 3. Composables & Stores
const router = useRouter();
// 4. Local state
const localValue = ref(0);
// 5. Computed properties
const displayValue = computed(() => props.count + localValue.value);
// 6. Methods
function handleUpdate() {
emit('update', localValue.value);
}
// 7. Lifecycle hooks
onMounted(() => {
// Initialization
});
</script>
<template>
<div class="component">
<!-- Template content -->
</div>
</template>
<style scoped lang="scss">
.component {
/* Styles */
}
</style>
5. Routing
Routes are defined in src/router/routes.ts. Use lazy loading for all page components:
{
path: '/feature',
component: () => import('layouts/MainLayout.vue'),
children: [
{
path: '',
component: () => import('pages/FeaturePage.vue'),
},
],
}
Development Guidelines
Package Management
Always use pnpm for package operations:
# Install dependencies
pnpm install
# Add a dependency
pnpm add package-name
# Add a dev dependency
pnpm add -D package-name
# Update dependencies
pnpm update
Development Workflow
# Start development server with hot-reload
pnpm dev
# Lint code
pnpm lint
# Format code
pnpm format
# Build for production
pnpm build
Code Style
- TypeScript: Use TypeScript for all new files
- Strict typing: Define interfaces and types for props, emits, and data structures
- Naming conventions:
- Components: PascalCase (e.g.,
ChatMessage.vue) - Composables: camelCase with
useprefix (e.g.,useChat.ts) - Stores: camelCase with
useprefix andStoresuffix (e.g.,useChatStore.ts) - Files: kebab-case or PascalCase for components
- Components: PascalCase (e.g.,
- Auto-formatting: Run
pnpm formatbefore committing
Quasar Components
Leverage Quasar's component library for UI elements. Quasar components are auto-imported, so you don't need explicit imports:
<script setup lang="ts">
// Quasar components are auto-imported - no need to import them
const text = ref('');
function handleSubmit() {
// Handle submission
}
</script>
<template>
<q-card>
<q-input v-model="text" label="Enter text" />
<q-btn label="Submit" @click="handleSubmit" />
</q-card>
</template>
To access Quasar utilities like $q, use the useQuasar composable:
<script setup lang="ts">
import { useQuasar } from 'quasar';
const $q = useQuasar();
function showNotification() {
$q.notify({
message: 'Hello World!',
color: 'primary',
});
}
</script>
Composables
Extract reusable logic into composables (place in feature-specific directories or src/composables/):
// composables/useFeature.ts
import { ref, computed } from 'vue';
export function useFeature() {
const state = ref(initialState);
const derivedValue = computed(() => {
return transform(state.value);
});
function updateState(newValue: StateType) {
state.value = newValue;
}
return {
state,
derivedValue,
updateState,
};
}
Common Patterns
1. API Calls
Create API service modules for external data fetching:
// features/chat/api/chatApi.ts
export const chatApi = {
async fetchMessages() {
const response = await fetch('/api/messages');
return response.json();
},
async sendMessage(message: string) {
const response = await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({ message }),
headers: { 'Content-Type': 'application/json' },
});
return response.json();
},
};
2. Form Handling
Use Quasar's form validation with reactive state (Quasar components are auto-imported):
<script setup lang="ts">
import { ref } from 'vue';
const formData = ref({
name: '',
email: '',
});
function onSubmit() {
// Handle form submission
}
</script>
<template>
<q-form @submit="onSubmit">
<q-input v-model="formData.name" label="Name" :rules="[(val) => !!val || 'Name is required']" />
<q-input
v-model="formData.email"
label="Email"
type="email"
:rules="[(val) => !!val || 'Email is required']"
/>
<q-btn type="submit" label="Submit" />
</q-form>
</template>
3. Loading and Error States
Manage async operations with consistent patterns:
<script setup lang="ts">
import { ref } from 'vue';
const loading = ref(false);
const error = ref<string | null>(null);
const data = ref<DataType | null>(null);
async function fetchData() {
loading.value = true;
error.value = null;
try {
data.value = await apiCall();
} catch (e) {
error.value = e instanceof Error ? e.message : 'An error occurred';
} finally {
loading.value = false;
}
}
</script>
<template>
<div>
<q-spinner v-if="loading" />
<q-banner v-else-if="error" class="text-negative">
{{ error }}
</q-banner>
<div v-else-if="data">
<!-- Display data -->
</div>
</div>
</template>
Testing
Currently, no test framework is configured. When adding tests:
- Consider using Vitest for unit tests
- Use @vue/test-utils for component testing
- Organize tests in
__tests__directories next to the code they test - Follow the naming convention:
*.spec.tsor*.test.ts
Best Practices
- Single Responsibility: Keep components, stores, and composables focused on a single concern
- Type Safety: Always define TypeScript types for props, emits, and complex data structures
- Immutability: Prefer immutable data operations where possible
- Reactivity: Be mindful of Vue's reactivity system; use
ref()andreactive()appropriately - Performance: Use
computed()for derived state,watch()for side effects - Code Reusability: Extract common logic into composables
- Feature Isolation: Keep features self-contained with minimal cross-dependencies
- Lazy Loading: Use lazy loading for routes and heavy components
- Error Handling: Implement proper error handling and user feedback
- Accessibility: Use semantic HTML and ARIA attributes where needed
AI Agent Instructions
When working on this project:
- Always use pnpm for package management operations
- Use Composition API with
<script setup lang="ts">for all Vue components - Organize by feature when adding new functionality
- Use Pinia stores for global state, with HMR support
- Type everything with TypeScript interfaces and types
- Follow Quasar conventions and use Quasar components
- Run linter and formatter before finalizing changes
- Keep components small and focused on a single responsibility
- Extract reusable logic into composables
- Use lazy loading for routes and heavy components