$collections */ public function __construct( public int $id, public string $uuid, public ?string $title, public string $model, public array $collections, public int $contextLimit, public int $messageCount, public int $totalTokensInput, public int $totalTokensOutput, public float $totalCostUsd, public \DateTimeImmutable $createdAt, public \DateTimeImmutable $lastActivity, public ?int $authorProfileId = null, ) { } /** * Create from database row with aggregated stats. * * @param array $row */ public static function fromDatabaseRow(array $row): self { $id = isset($row['id']) ? (int) $row['id'] : 0; $uuid = isset($row['uuid']) ? (string) $row['uuid'] : ''; $title = isset($row['title']) ? (string) $row['title'] : null; $model = isset($row['model']) ? (string) $row['model'] : 'unknown'; $collections = self::parseCollections($row['collections'] ?? ''); $contextLimit = isset($row['context_limit']) ? (int) $row['context_limit'] : 3; $messageCount = isset($row['message_count']) ? (int) $row['message_count'] : 0; $totalTokensInput = isset($row['total_tokens_input']) ? (int) $row['total_tokens_input'] : 0; $totalTokensOutput = isset($row['total_tokens_output']) ? (int) $row['total_tokens_output'] : 0; $totalCostUsd = isset($row['total_cost_usd']) ? (float) $row['total_cost_usd'] : 0.0; $createdAtStr = isset($row['created_at']) ? (string) $row['created_at'] : 'now'; $lastActivityStr = isset($row['last_activity']) ? (string) $row['last_activity'] : (isset($row['created_at']) ? (string) $row['created_at'] : 'now'); $authorProfileId = isset($row['author_profile_id']) ? (int) $row['author_profile_id'] : null; return new self( id: $id, uuid: $uuid, title: $title, model: $model, collections: $collections, contextLimit: $contextLimit, messageCount: $messageCount, totalTokensInput: $totalTokensInput, totalTokensOutput: $totalTokensOutput, totalCostUsd: $totalCostUsd, createdAt: new \DateTimeImmutable($createdAtStr), lastActivity: new \DateTimeImmutable($lastActivityStr), authorProfileId: $authorProfileId, ); } /** * Parse comma-separated collections or JSON array. * * @return array */ private static function parseCollections(mixed $collections): array { if (is_array($collections)) { /** @var array $collections */ return array_values(array_map(fn (mixed $v): string => (string) $v, $collections)); } if (!is_string($collections) || $collections === '') { return []; } $decoded = json_decode($collections, true); if (is_array($decoded)) { return array_values(array_map(fn (mixed $v): string => (string) $v, $decoded)); } return array_values(array_filter(array_map('trim', explode(',', $collections)))); } /** * Get total tokens used. */ public function totalTokens(): int { return $this->totalTokensInput + $this->totalTokensOutput; } /** * Get display title (with fallback). */ public function displayTitle(): string { return $this->title ?? 'Chat vom ' . $this->createdAt->format('d.m.Y H:i'); } /** * Check if session is active (has recent activity). */ public function isRecentlyActive(int $hoursThreshold = 24): bool { $threshold = new \DateTimeImmutable("-{$hoursThreshold} hours"); return $this->lastActivity > $threshold; } }