ITP WiP
This commit is contained in:
152
tools/itb/README.md
Normal file
152
tools/itb/README.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# ITB: The Interactive Text Browser
|
||||
|
||||
ITB aims to create a set of Python scripts to enable a text-based AI agent to interact with web pages.
|
||||
The system is designed to start a detached browser process and interact with it through subsequent scripts.
|
||||
Each script performs a specific action, such as starting the browser, taking a virtual screenshot, scrolling, clicking, and inputting text.
|
||||
The output of these scripts is optimized to be evaluated by a Large Language Model (LLM).
|
||||
So it can understand and interact with the webpage in a text-only manner.
|
||||
|
||||
# The Scripts
|
||||
|
||||
## itb_start
|
||||
|
||||
Starts a detached browser session and outputs the necessary information to interact with the browser process.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_start <url>
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
- Process ID (PID) of the browser session.
|
||||
- WebDriver session ID.
|
||||
|
||||
### Implementation Strategies
|
||||
|
||||
Use Selenium to start a browser session.
|
||||
Ensure the browser runs in a detached process to allow other scripts to interact with it.
|
||||
Output the PID and WebDriver session ID for subsequent scripts to use.
|
||||
|
||||
## itb_screenshot
|
||||
|
||||
Starts a virtual screenshot of the current browser view and outputs all visible information in text form.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_screenshot <session_id>
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
- Scroll offset, visible range and limits.
|
||||
- Text representation of all currently visible elements.
|
||||
- IDs, size and offset for interactive elements (e.g., buttons, links, input fields).
|
||||
|
||||
### Implementation Strategies
|
||||
|
||||
Get the identifiers for all visible elements.
|
||||
Identify elements with text content and interaction capabilities.
|
||||
Extract text content without markup.
|
||||
Create simplified XML structure.
|
||||
|
||||
## itb_scroll
|
||||
|
||||
Scrolls the browser window to a specified position.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_scroll <session_id> <x> <y>
|
||||
```
|
||||
|
||||
## itb_click
|
||||
|
||||
Clicks on a specified element in the browser.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_click <session_id> -e <element_id>
|
||||
itb_click <session_id> -x <x> -y <y>
|
||||
```
|
||||
|
||||
### Implementation Strategies
|
||||
|
||||
Use a random short time to slowly move the cursor to the target position.
|
||||
Click on a random position within the target element.
|
||||
|
||||
## itb_input
|
||||
|
||||
Inputs text or keystrokes into the focussed element.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_input <session_id> -t <text>
|
||||
itb_input <session_id> -k <keystroke> ...
|
||||
```
|
||||
|
||||
### Implementation Strategies
|
||||
|
||||
Use a random short delay between adding each character of text, to simulate human-like typing.
|
||||
Keystrokes are additive.
|
||||
e.g. for CTRL SHIFT a, this emulates the user pressing CTRL, keeping it pressed, then pressing SHIFT, keeping both CTRL and SHIFT pressed, then pressing 'a', keeping all three pressed, then releasing all three.
|
||||
|
||||
## itb_links
|
||||
|
||||
Extracts all links from the full webpage.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_links <session_id>
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
- List of all visible links with their text, URLs and position on the page.
|
||||
|
||||
## itb_forms
|
||||
|
||||
Extracts all visible forms and their input fields from the current browser view.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_forms <session_id>
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
List of all visible forms with their
|
||||
- input fields
|
||||
- connected labels
|
||||
- identifier
|
||||
- position on the page.
|
||||
|
||||
## itb_navigate
|
||||
|
||||
Navigates to a specified URL within the existing browser session.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_navigate <session_id> <url>
|
||||
```
|
||||
|
||||
## itb_refresh
|
||||
|
||||
Refreshes the current browser page.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
itb_refresh <session_id>
|
||||
```
|
||||
|
||||
### Implementation Strategies
|
||||
|
||||
Wait till the page is fully loaded and the DOM is stable.
|
||||
0
tools/itb/bin/itb_click
Executable file
0
tools/itb/bin/itb_click
Executable file
0
tools/itb/bin/itb_forms
Executable file
0
tools/itb/bin/itb_forms
Executable file
0
tools/itb/bin/itb_input
Executable file
0
tools/itb/bin/itb_input
Executable file
0
tools/itb/bin/itb_links
Executable file
0
tools/itb/bin/itb_links
Executable file
0
tools/itb/bin/itb_navigate
Executable file
0
tools/itb/bin/itb_navigate
Executable file
0
tools/itb/bin/itb_refresh
Executable file
0
tools/itb/bin/itb_refresh
Executable file
180
tools/itb/bin/itb_screenshot
Executable file
180
tools/itb/bin/itb_screenshot
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Dict, List, Set, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class DOMNode:
|
||||
element: WebElement
|
||||
children: List['DOMNode']
|
||||
own_text: str
|
||||
total_text: str
|
||||
tag: str
|
||||
location: Dict[str, int]
|
||||
size: Dict[str, int]
|
||||
is_interactive: bool
|
||||
attributes: Dict[str, str]
|
||||
|
||||
class DOMTree:
|
||||
def __init__(self, root_element: WebElement):
|
||||
self.root = self._build_tree(root_element)
|
||||
|
||||
def _get_element_own_text(self, element: WebElement) -> str:
|
||||
script = """
|
||||
let element = arguments[0];
|
||||
let walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: function(node) {
|
||||
return node.parentElement === element ?
|
||||
NodeFilter.FILTER_ACCEPT :
|
||||
NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
}
|
||||
);
|
||||
let text = '';
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
text += node.textContent;
|
||||
}
|
||||
return text.trim();
|
||||
"""
|
||||
return element.parent.execute_script(script, element)
|
||||
|
||||
def _is_interactive(self, element: WebElement) -> bool:
|
||||
return (
|
||||
element.tag_name in {"button", "a", "input", "select", "textarea"} or
|
||||
element.get_attribute("role") in {"button", "link", "textbox"} or
|
||||
element.get_attribute("onclick") is not None or
|
||||
element.get_attribute("contenteditable") == "true"
|
||||
)
|
||||
|
||||
def _get_attributes(self, element: WebElement) -> Dict[str, str]:
|
||||
attrs = {}
|
||||
for attr in ['placeholder', 'aria-label', 'alt', 'title', 'role']:
|
||||
if value := element.get_attribute(attr):
|
||||
attrs[attr] = value
|
||||
return attrs
|
||||
|
||||
def _build_tree(self, element: WebElement) -> DOMNode:
|
||||
children = [
|
||||
self._build_tree(child)
|
||||
for child in element.find_elements(By.XPATH, "./*")
|
||||
]
|
||||
|
||||
own_text = self._get_element_own_text(element)
|
||||
total_text = own_text + " " + " ".join(
|
||||
child.total_text for child in children
|
||||
)
|
||||
total_text = " ".join(total_text.split())
|
||||
|
||||
return DOMNode(
|
||||
element=element,
|
||||
children=children,
|
||||
own_text=own_text,
|
||||
total_text=total_text,
|
||||
tag=element.tag_name,
|
||||
location=element.location,
|
||||
size=element.size,
|
||||
is_interactive=self._is_interactive(element),
|
||||
attributes=self._get_attributes(element)
|
||||
)
|
||||
|
||||
class ScreenshotGenerator:
|
||||
IGNORED_TAGS = {'script', 'style', 'meta', 'link', 'noscript'}
|
||||
|
||||
def __init__(self, debug_port: int):
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_experimental_option("debuggerAddress", f"127.0.0.1:{debug_port}")
|
||||
self.driver = webdriver.Chrome(options=options)
|
||||
|
||||
def get_viewport_metrics(self) -> Dict[str, int]:
|
||||
return {
|
||||
"scroll_x": self.driver.execute_script("return window.pageXOffset;"),
|
||||
"scroll_y": self.driver.execute_script("return window.pageYOffset;"),
|
||||
"width": self.driver.execute_script("return document.documentElement.clientWidth;"),
|
||||
"height": self.driver.execute_script("return document.documentElement.clientHeight;"),
|
||||
"max_scroll_y": self.driver.execute_script(
|
||||
"return Math.max(document.documentElement.scrollHeight - window.innerHeight, 0);"
|
||||
)
|
||||
}
|
||||
|
||||
def should_process_node(self, node: DOMNode) -> bool:
|
||||
# Skip invisible elements
|
||||
if not node.element.is_displayed():
|
||||
return False
|
||||
|
||||
# Skip script and style elements
|
||||
if node.tag in self.IGNORED_TAGS:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_xml_element(self, node: DOMNode, parent_element: Optional[ET.Element] = None) -> Optional[ET.Element]:
|
||||
attrs = {
|
||||
"x": str(node.location["x"]),
|
||||
"y": str(node.location["y"]),
|
||||
"width": str(node.size["width"]),
|
||||
"height": str(node.size["height"])
|
||||
}
|
||||
|
||||
if node.is_interactive:
|
||||
element = ET.Element("interactive", attrs)
|
||||
child = ET.SubElement(element, node.tag)
|
||||
for attr, value in node.attributes.items():
|
||||
child.set(attr, value)
|
||||
if node.own_text:
|
||||
child.text = node.own_text
|
||||
return element
|
||||
|
||||
element = ET.Element(node.tag, attrs)
|
||||
if node.own_text:
|
||||
element.text = node.own_text
|
||||
return element
|
||||
|
||||
def process_tree(self, node: DOMNode, parent_element: Optional[ET.Element] = None) -> List[ET.Element]:
|
||||
if not self.should_process_node(node):
|
||||
return []
|
||||
|
||||
current_element = self.create_xml_element(node, parent_element)
|
||||
if not current_element:
|
||||
return []
|
||||
|
||||
for child in node.children:
|
||||
child_elements = self.process_tree(child, current_element)
|
||||
for child_element in child_elements:
|
||||
current_element.append(child_element)
|
||||
|
||||
return [current_element]
|
||||
|
||||
def generate_output(self) -> ET.Element:
|
||||
metrics = self.get_viewport_metrics()
|
||||
viewport = ET.Element("viewport", {
|
||||
"scroll_x": str(metrics["scroll_x"]),
|
||||
"scroll_y": str(metrics["scroll_y"]),
|
||||
"width": str(metrics["width"]),
|
||||
"height": str(metrics["height"]),
|
||||
"max_scroll_y": str(metrics["max_scroll_y"])
|
||||
})
|
||||
|
||||
dom_tree = DOMTree(self.driver.find_element(By.TAG_NAME, "body"))
|
||||
for element in self.process_tree(dom_tree.root):
|
||||
viewport.append(element)
|
||||
|
||||
return viewport
|
||||
|
||||
def main(debug_port: int):
|
||||
generator = ScreenshotGenerator(debug_port)
|
||||
output = generator.generate_output()
|
||||
print(ET.tostring(output, encoding="unicode", method="xml"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: itb_screenshot <debug_port>")
|
||||
sys.exit(1)
|
||||
main(int(sys.argv[1]))
|
||||
0
tools/itb/bin/itb_scroll
Executable file
0
tools/itb/bin/itb_scroll
Executable file
33
tools/itb/bin/itb_start
Executable file
33
tools/itb/bin/itb_start
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def start(url):
|
||||
debug_port = random.randint(9000, 9999)
|
||||
chrome_cmd = [
|
||||
'/usr/bin/google-chrome',
|
||||
'--headless=new',
|
||||
'--no-sandbox',
|
||||
'--remote-debugging-port=' + str(debug_port),
|
||||
url
|
||||
]
|
||||
|
||||
process = subprocess.Popen(
|
||||
chrome_cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True
|
||||
)
|
||||
|
||||
print({
|
||||
"pid": process.pid,
|
||||
"debug_port": debug_port
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: itb_start <url>")
|
||||
sys.exit(1)
|
||||
start(sys.argv[1])
|
||||
11
tools/itb/itb.egg-info/PKG-INFO
Normal file
11
tools/itb/itb.egg-info/PKG-INFO
Normal file
@@ -0,0 +1,11 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: itb
|
||||
Version: 0.1.0
|
||||
Summary: UNKNOWN
|
||||
Home-page: UNKNOWN
|
||||
License: UNKNOWN
|
||||
Platform: UNKNOWN
|
||||
Provides-Extra: dev
|
||||
|
||||
UNKNOWN
|
||||
|
||||
16
tools/itb/itb.egg-info/SOURCES.txt
Normal file
16
tools/itb/itb.egg-info/SOURCES.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
README.md
|
||||
setup.py
|
||||
bin/itb_click
|
||||
bin/itb_forms
|
||||
bin/itb_input
|
||||
bin/itb_links
|
||||
bin/itb_navigate
|
||||
bin/itb_refresh
|
||||
bin/itb_screenshot
|
||||
bin/itb_scroll
|
||||
bin/itb_start
|
||||
itb.egg-info/PKG-INFO
|
||||
itb.egg-info/SOURCES.txt
|
||||
itb.egg-info/dependency_links.txt
|
||||
itb.egg-info/requires.txt
|
||||
itb.egg-info/top_level.txt
|
||||
1
tools/itb/itb.egg-info/dependency_links.txt
Normal file
1
tools/itb/itb.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
10
tools/itb/itb.egg-info/requires.txt
Normal file
10
tools/itb/itb.egg-info/requires.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
beautifulsoup4>=4.9.0
|
||||
click>=8.0.0
|
||||
selenium>=4.0.0
|
||||
webdriver-manager>=3.8.0
|
||||
|
||||
[dev]
|
||||
black>=22.0.0
|
||||
flake8>=4.0.0
|
||||
pytest-cov>=4.0.0
|
||||
pytest>=7.0.0
|
||||
1
tools/itb/itb.egg-info/top_level.txt
Normal file
1
tools/itb/itb.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
8
tools/itb/requirements.txt
Normal file
8
tools/itb/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
selenium>=4.0.0
|
||||
webdriver-manager>=3.8.0
|
||||
click>=8.0.0
|
||||
beautifulsoup4>=4.9.0
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
black>=22.0.0
|
||||
flake8>=4.0.0
|
||||
38
tools/itb/result.social.html
Normal file
38
tools/itb/result.social.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<viewport scroll_x="0" scroll_y="0" width="780" height="441" max_scroll_y="1200">
|
||||
<nav x="0" y="0" width="100%" height="60">
|
||||
<h1>SocialConnect</h1>
|
||||
<interactive x="16" y="0" width="240" height="40">
|
||||
<input placeholder="Search posts..." />
|
||||
</interactive>
|
||||
</nav>
|
||||
|
||||
<div class="post" x="20" y="80" width="680" height="400">
|
||||
<div class="username" x="72" y="96" width="200" height="20">Alice Johnson</div>
|
||||
<div class="timestamp" x="72" y="116" width="200" height="16">2 hours ago</div>
|
||||
|
||||
<div class="post-content" x="36" y="148" width="648" height="60">
|
||||
Just finished my morning hike! The sunrise was absolutely breathtaking today. 🌄 #MorningHike #Nature #Wellness
|
||||
</div>
|
||||
|
||||
<img x="36" y="220" width="648" height="400" alt="Sunrise view from mountain top" />
|
||||
|
||||
<div class="actions" x="36" y="632" width="648" height="40">
|
||||
<interactive x="36" y="632" width="216" height="40">
|
||||
<button>Like</button>
|
||||
</interactive>
|
||||
<interactive x="252" y="632" width="216" height="40">
|
||||
<button>Comment</button>
|
||||
</interactive>
|
||||
<interactive x="468" y="632" width="216" height="40">
|
||||
<button>Share</button>
|
||||
</interactive>
|
||||
</div>
|
||||
|
||||
<div class="comment" x="44" y="684" width="632" height="60">
|
||||
<div class="username" x="96" y="684" width="200" height="20">Bob Smith</div>
|
||||
<div class="comment-content" x="96" y="704" width="580" height="40">
|
||||
Beautiful view! Which trail is this?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</viewport>
|
||||
32
tools/itb/result.test.html
Normal file
32
tools/itb/result.test.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<viewport scroll_y="0" scroll_x="0" height="800" width="1024">
|
||||
<header class="sticky" y="0" x="0" width="1024" height="50">
|
||||
<form id="search">
|
||||
<input type="text" placeholder="Search..." aria-label="Search box" y="10" x="10" width="200" height="30" />
|
||||
<button type="submit" y="10" x="220" width="30" height="30">🔍</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<section class="scrollable" y="50" x="0" width="1024" height="200" scroll_y="0" max_scroll="400">
|
||||
<heading level="2">Scrollable Section</heading>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit... [first paragraph]
|
||||
</section>
|
||||
|
||||
<interactive y="260" x="10" width="100" height="30">
|
||||
<button onclick="showModal()">Show Modal</button>
|
||||
</interactive>
|
||||
|
||||
<interactive y="260" x="120" width="120" height="30">
|
||||
<link href="#">JavaScript Link</link>
|
||||
</interactive>
|
||||
|
||||
<interactive y="260" x="250" width="100" height="30">
|
||||
<button role="button" tabindex="0">ARIA Button</button>
|
||||
</interactive>
|
||||
|
||||
<table y="300" x="0" width="1024">
|
||||
<cell>Cell with mixed formatting</cell>
|
||||
<cell>Cell with list:
|
||||
- Item 1
|
||||
- Item 2</cell>
|
||||
</table>
|
||||
</viewport>
|
||||
32
tools/itb/setup.py
Normal file
32
tools/itb/setup.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="itb",
|
||||
version="0.1.0",
|
||||
packages=find_packages(),
|
||||
scripts=[
|
||||
'bin/itb_start',
|
||||
'bin/itb_screenshot',
|
||||
'bin/itb_scroll',
|
||||
'bin/itb_click',
|
||||
'bin/itb_input',
|
||||
'bin/itb_links',
|
||||
'bin/itb_forms',
|
||||
'bin/itb_navigate',
|
||||
'bin/itb_refresh'
|
||||
],
|
||||
install_requires=[
|
||||
'selenium>=4.0.0',
|
||||
'webdriver-manager>=3.8.0',
|
||||
'click>=8.0.0',
|
||||
'beautifulsoup4>=4.9.0'
|
||||
],
|
||||
extras_require={
|
||||
'dev': [
|
||||
'pytest>=7.0.0',
|
||||
'pytest-cov>=4.0.0',
|
||||
'black>=22.0.0',
|
||||
'flake8>=4.0.0'
|
||||
]
|
||||
}
|
||||
)
|
||||
178
tools/itb/social.html
Normal file
178
tools/itb/social.html
Normal file
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SocialConnect</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.search-box {
|
||||
margin-left: 16px;
|
||||
padding: 8px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #ddd;
|
||||
width: 240px;
|
||||
}
|
||||
.main-content {
|
||||
margin-top: 80px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 680px;
|
||||
padding: 20px;
|
||||
}
|
||||
.post {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
.post-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.profile-pic {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
background: #ddd;
|
||||
}
|
||||
.post-meta {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.username {
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.timestamp {
|
||||
color: #65676b;
|
||||
font-size: 13px;
|
||||
}
|
||||
.post-content {
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.post-image {
|
||||
width: 100%;
|
||||
max-height: 400px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.post-actions {
|
||||
display: flex;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.action-button {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #65676b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.action-button:hover {
|
||||
background: #f2f2f2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.comment-section {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.comment {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.comment-content {
|
||||
background: #f0f2f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 18px;
|
||||
margin-left: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<h1>SocialConnect</h1>
|
||||
<input type="text" class="search-box" placeholder="Search posts..." aria-label="Search posts">
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- Generate 20 posts with varying content -->
|
||||
<div class="post">
|
||||
<div class="post-header">
|
||||
<div class="profile-pic"></div>
|
||||
<div class="post-meta">
|
||||
<div class="username">Alice Johnson</div>
|
||||
<div class="timestamp">2 hours ago</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
Just finished my morning hike! The sunrise was absolutely breathtaking today. 🌄
|
||||
#MorningHike #Nature #Wellness
|
||||
</div>
|
||||
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='648' height='400'><rect width='100%' height='100%' fill='%23ddd'/></svg>" alt="Sunrise view from mountain top" class="post-image">
|
||||
<div class="post-actions">
|
||||
<button class="action-button">Like</button>
|
||||
<button class="action-button">Comment</button>
|
||||
<button class="action-button">Share</button>
|
||||
</div>
|
||||
<div class="comment-section">
|
||||
<div class="comment">
|
||||
<div class="profile-pic"></div>
|
||||
<div class="comment-content">
|
||||
<div class="username">Bob Smith</div>
|
||||
Beautiful view! Which trail is this?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repeat similar structure with different content 20 times -->
|
||||
<!-- This is post 2 -->
|
||||
<div class="post">
|
||||
<div class="post-header">
|
||||
<div class="profile-pic"></div>
|
||||
<div class="post-meta">
|
||||
<div class="username">Sarah Chen</div>
|
||||
<div class="timestamp">3 hours ago</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
Just launched my new portfolio website! 🚀 Excited to share my latest projects with everyone.
|
||||
Check it out and let me know what you think! #WebDevelopment #Portfolio #Coding
|
||||
</div>
|
||||
<div class="post-actions">
|
||||
<button class="action-button">Like</button>
|
||||
<button class="action-button">Comment</button>
|
||||
<button class="action-button">Share</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Continue with more posts... -->
|
||||
<!-- Posts 3-20 would follow similar pattern with varied content -->
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
90
tools/itb/test.html
Normal file
90
tools/itb/test.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.scrollable-div {
|
||||
height: 200px;
|
||||
overflow-y: scroll;
|
||||
border: 1px solid black;
|
||||
}
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: white;
|
||||
}
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Sticky header with form -->
|
||||
<header class="sticky-header">
|
||||
<form id="search">
|
||||
<input type="text" placeholder="Search..." aria-label="Search box">
|
||||
<button type="submit">🔍</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<!-- Nested scrollable content -->
|
||||
<div class="scrollable-div">
|
||||
<h2>Scrollable Section</h2>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam a bibendum purus, id accumsan lorem. Ut quis diam semper, rutrum justo in, ornare urna. Proin vel consectetur odio. Donec quis odio ut erat lobortis aliquam. Vestibulum vitae tellus interdum, hendrerit odio id, varius urna. Sed nec est tempor, placerat lectus sit amet, aliquam mauris. Duis elementum risus vitae nunc mollis ullamcorper. Donec sodales ac neque at aliquam. Duis eu risus ex. Fusce neque erat, egestas nec placerat in, dapibus quis magna. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut faucibus nunc tellus, vel consequat dolor suscipit vel. Sed sapien erat, rutrum non dignissim nec, egestas at augue. Nunc interdum lacinia urna a euismod. Integer ullamcorper sit amet sapien non auctor.
|
||||
</p>
|
||||
<p>
|
||||
Nam vitae bibendum nulla, ut tempus enim. Aenean eu iaculis metus, vel rhoncus justo. Sed vel interdum sem. Praesent congue erat sit amet ultrices molestie. Pellentesque pellentesque odio a ullamcorper facilisis. Sed at viverra magna. Aliquam posuere dolor tortor, non aliquam orci bibendum eu. Sed efficitur feugiat congue. Vivamus elementum in metus lacinia gravida.
|
||||
</p>
|
||||
<p>
|
||||
Fusce aliquam, erat ac consequat eleifend, massa neque posuere urna, vel sagittis tortor neque hendrerit augue. Nulla mattis felis non efficitur hendrerit. Pellentesque viverra elementum enim ornare pellentesque. Donec mollis eget leo non congue. Aliquam vel iaculis odio. Mauris eget neque suscipit, tempus mi quis, porta quam. Integer non tempor leo. Pellentesque magna nisl, fermentum sit amet orci ut, posuere dictum erat. Vivamus a lacinia lacus, eleifend ultrices velit. Etiam est arcu, dictum at sapien efficitur, fringilla tincidunt sem. In nec lacus quis diam placerat sollicitudin. Vivamus scelerisque commodo velit, id lacinia orci volutpat quis. Nulla facilisi. Nam nec quam quis lorem vulputate auctor quis a dui.
|
||||
</p>
|
||||
<p>
|
||||
Curabitur a erat nisi. Fusce lacinia, enim quis mollis placerat, eros nulla blandit turpis, id feugiat purus enim ut diam. Etiam et tincidunt turpis, in scelerisque lectus. Nunc vulputate at velit sit amet tincidunt. Cras maximus ultrices enim, sit amet placerat nisi tristique eget. Sed ac neque sed quam laoreet pellentesque nec vel lorem. Nunc sollicitudin arcu ac quam tempor bibendum. Phasellus sollicitudin tortor at porta lobortis. Aenean venenatis ultrices neque ut scelerisque. Nullam aliquam fringilla varius. Etiam eget nunc leo. Proin ornare tempus libero eget molestie. Praesent id ex sit amet ante semper dictum. Mauris ac libero at ipsum dapibus pretium quis at sapien. Praesent ac luctus lacus. Praesent id varius nisl, et luctus ex.
|
||||
</p>
|
||||
<iframe src="about:blank" title="Embedded content">
|
||||
<p>
|
||||
Sed ullamcorper sapien non nunc venenatis rutrum. Nulla mollis eu augue non mollis. Maecenas molestie congue libero. Nulla non mi sem. Cras sed ipsum et orci porta posuere et in metus. Vivamus gravida leo a risus interdum, et aliquet neque pretium. Duis a mauris nec erat tempus lacinia. Cras a justo vel dui varius mattis sed ac neque. Phasellus blandit massa sed elit consectetur placerat. Sed aliquet et lacus nec posuere. Donec mattis dolor id tortor facilisis consectetur. Phasellus sit amet magna non nisl pretium semper. Sed viverra, mauris vitae ultricies interdum, libero felis aliquet massa, at vehicula elit lacus a diam. Aliquam sed erat et justo tempor mattis eget et neque. Ut fermentum magna ut lorem sodales condimentum. Vestibulum elementum nec urna sed suscipit.
|
||||
</p>
|
||||
</iframe>
|
||||
<div contenteditable="true">
|
||||
Curabitur a erat nisi. Fusce lacinia, enim quis mollis placerat, eros nulla blandit turpis, id feugiat purus enim ut diam. Etiam et tincidunt turpis, in scelerisque lectus. Nunc vulputate at velit sit amet tincidunt. Cras maximus ultrices enim, sit amet placerat nisi tristique eget. Sed ac neque sed quam laoreet pellentesque nec vel lorem. Nunc sollicitudin arcu ac quam tempor bibendum. Phasellus sollicitudin tortor at porta lobortis. Aenean venenatis ultrices neque ut scelerisque. Nullam aliquam fringilla varius. Etiam eget nunc leo. Proin ornare tempus libero eget molestie. Praesent id ex sit amet ante semper dictum. Mauris ac libero at ipsum dapibus pretium quis at sapien. Praesent ac luctus lacus. Praesent id varius nisl, et luctus ex.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive elements with dynamic content -->
|
||||
<div>
|
||||
<button onclick="showModal()">Show Modal</button>
|
||||
<a href="#" onclick="return false;">JavaScript Link</a>
|
||||
<div role="button" tabindex="0">ARIA Button</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal with overlay -->
|
||||
<div class="modal" style="display:none">
|
||||
<h3>Modal Title</h3>
|
||||
<form>
|
||||
<select multiple>
|
||||
<option>Choice 1</option>
|
||||
<option>Choice 2</option>
|
||||
</select>
|
||||
<input type="range" min="0" max="100">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Complex text layout -->
|
||||
<table>
|
||||
<tr>
|
||||
<td>Cell with <strong>mixed</strong> <em>formatting</em></td>
|
||||
<td>Cell with list:
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user